diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0bd3b30814..2d1fdafa30 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,6 +26,7 @@ The `/api/something` endpoint is now `/api/something-else` ## Checklist: +- [ ] I have carefully read CONTRIBUTING.md - [ ] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation if applicable - [ ] I have no unrelated changes in the PR. diff --git a/.github/workflows/close-llm-pr.yml b/.github/workflows/close-llm-pr.yml new file mode 100644 index 0000000000..511d5c7f55 --- /dev/null +++ b/.github/workflows/close-llm-pr.yml @@ -0,0 +1,38 @@ +name: Close LLM-generated PRs + +on: + pull_request_target: + types: [labeled] + +permissions: {} + +jobs: + comment_and_close: + runs-on: ubuntu-latest + if: ${{ github.event.label.name == 'llm-generated' }} + permissions: + pull-requests: write + steps: + - name: Comment and close + env: + GH_TOKEN: ${{ github.token }} + NODE_ID: ${{ github.event.pull_request.node_id }} + run: | + gh api graphql \ + -f prId="$NODE_ID" \ + -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our [CONTRIBUTING.md](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md#use-of-generative-ai), we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \ + -f query=' + mutation CommentAndClosePR($prId: ID!, $body: String!) { + addComment(input: { + subjectId: $prId, + body: $body + }) { + __typename + } + + closePullRequest(input: { + pullRequestId: $prId + }) { + __typename + } + }' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28a74ff33f..93efccf2e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -497,14 +497,15 @@ jobs: run: npx playwright install chromium --only-shell if: ${{ !cancelled() }} - name: Docker build - run: docker compose build + run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 if: ${{ !cancelled() }} - name: Run e2e tests (web) env: CI: true - run: npx playwright test --project=chromium + PLAYWRIGHT_DISABLE_WEBSERVER: true + run: npx playwright test --project=web if: ${{ !cancelled() }} - - name: Archive web results + - name: Archive e2e test (web) results uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: success() || failure() with: @@ -513,14 +514,37 @@ jobs: - name: Run ui tests (web) env: CI: true + PLAYWRIGHT_DISABLE_WEBSERVER: true run: npx playwright test --project=ui if: ${{ !cancelled() }} - - name: Archive ui results + - name: Archive ui test (web) results uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: success() || failure() with: name: e2e-ui-test-results-${{ matrix.runner }} path: e2e/playwright-report/ + - name: Run maintenance tests + env: + CI: true + PLAYWRIGHT_DISABLE_WEBSERVER: true + run: npx playwright test --project=maintenance + if: ${{ !cancelled() }} + - name: Archive maintenance tests (web) results + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: success() || failure() + with: + name: e2e-maintenance-isolated-test-results-${{ matrix.runner }} + path: e2e/playwright-report/ + - name: Capture Docker logs + if: always() + run: docker compose logs --no-color > docker-compose-logs.txt + working-directory: ./e2e + - name: Archive Docker logs + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: always() + with: + name: docker-compose-logs-${{ matrix.runner }} + path: e2e/docker-compose-logs.txt success-check-e2e: name: End-to-End Tests Success needs: [e2e-tests-server-cli, e2e-tests-web] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 109708cc6e..1695403cb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ If you are looking for something to work on, there are discussions and issues wi ## Use of generative AI -We generally discourage PRs entirely generated by an LLM. For any part generated by an LLM, please put extra effort into your self-review. By using generative AI without proper self-review, the time you save ends up being more work we need to put in for proper reviews and code cleanup. Please keep that in mind when submitting code by an LLM. Clearly state the use of LLMs/(generative) AI in your pull request as requested by the template. +We ask you not to open PRs generated with an LLM. We find that code generated like this tends to need a large amount of back-and-forth, which is a very inefficient use of our time. If we want LLM-generated code, it's much faster for us to use an LLM ourselves than to go through an intermediary via a pull request. ## Feature freezes diff --git a/cli/package.json b/cli/package.json index 3d3bae1914..d80efdd74a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.5.5", + "version": "2.5.6", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -14,13 +14,13 @@ ], "devDependencies": { "@eslint/js": "^9.8.0", - "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/sdk": "workspace:*", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^24.10.9", + "@types/node": "^24.10.11", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index ff7b609eef..42c33491f2 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -4,6 +4,7 @@ import { AssetBulkUploadCheckResult, AssetMediaResponseDto, AssetMediaStatus, + Permission, addAssetsToAlbum, checkBulkUpload, createAlbum, @@ -20,13 +21,11 @@ import { Stats, createReadStream } from 'node:fs'; import { stat, unlink } from 'node:fs/promises'; import path, { basename } from 'node:path'; import { Queue } from 'src/queue'; -import { BaseOptions, Batcher, authenticate, crawl, sha1 } from 'src/utils'; +import { BaseOptions, Batcher, authenticate, crawl, requirePermissions, s, sha1 } from 'src/utils'; const UPLOAD_WATCH_BATCH_SIZE = 100; const UPLOAD_WATCH_DEBOUNCE_TIME_MS = 10_000; -const s = (count: number) => (count === 1 ? '' : 's'); - // TODO figure out why `id` is missing type AssetBulkUploadCheckResults = Array; type Asset = { id: string; filepath: string }; @@ -136,6 +135,7 @@ export const startWatch = async ( export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => { await authenticate(baseOptions); + await requirePermissions([Permission.AssetUpload]); const scanFiles = await scan(paths, options); @@ -180,18 +180,49 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas } let multiBar: MultiBar | undefined; + let totalSize = 0; + const statsMap = new Map(); + + // Calculate total size first + for (const filepath of files) { + const stats = await stat(filepath); + statsMap.set(filepath, stats); + totalSize += stats.size; + } if (progress) { multiBar = new MultiBar( - { format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + { + format: '{message} | {bar} | {percentage}% | ETA: {eta_formatted} | {value}/{total}', + formatValue: (v: number, options, type) => { + // Don't format percentage + if (type === 'percentage') { + return v.toString(); + } + return byteSize(v).toString(); + }, + etaBuffer: 100, // Increase samples for ETA calculation + }, Presets.shades_classic, ); + + // Ensure we restore cursor on interrupt + process.on('SIGINT', () => { + if (multiBar) { + multiBar.stop(); + } + process.exit(0); + }); } else { - console.log(`Received ${files.length} files, hashing...`); + console.log(`Received ${files.length} files (${byteSize(totalSize)}), hashing...`); } - const hashProgressBar = multiBar?.create(files.length, 0, { message: 'Hashing files ' }); - const checkProgressBar = multiBar?.create(files.length, 0, { message: 'Checking for duplicates' }); + const hashProgressBar = multiBar?.create(totalSize, 0, { + message: 'Hashing files ', + }); + const checkProgressBar = multiBar?.create(totalSize, 0, { + message: 'Checking for duplicates', + }); const newFiles: string[] = []; const duplicates: Asset[] = []; @@ -211,7 +242,13 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas } } - checkProgressBar?.increment(assets.length); + // Update progress based on total size of processed files + let processedSize = 0; + for (const asset of assets) { + const stats = statsMap.get(asset.id); + processedSize += stats?.size || 0; + } + checkProgressBar?.increment(processedSize); }, { concurrency, retry: 3 }, ); @@ -221,6 +258,10 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas const queue = new Queue( async (filepath: string): Promise => { + const stats = statsMap.get(filepath); + if (!stats) { + throw new Error(`Stats not found for ${filepath}`); + } const dto = { id: filepath, checksum: await sha1(filepath) }; results.push(dto); @@ -231,7 +272,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas void checkBulkUploadQueue.push(batch); } - hashProgressBar?.increment(); + hashProgressBar?.increment(stats.size); return results; }, { concurrency, retry: 3 }, diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts index f0011c6a24..1e1efa97b4 100644 --- a/cli/src/commands/auth.ts +++ b/cli/src/commands/auth.ts @@ -1,7 +1,15 @@ -import { getMyUser } from '@immich/sdk'; +import { getMyUser, Permission } from '@immich/sdk'; import { existsSync } from 'node:fs'; import { mkdir, unlink } from 'node:fs/promises'; -import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; +import { + BaseOptions, + connect, + getAuthFilePath, + logError, + requirePermissions, + withError, + writeAuthFile, +} from 'src/utils'; export const login = async (url: string, key: string, options: BaseOptions) => { console.log(`Logging in to ${url}`); @@ -9,6 +17,7 @@ export const login = async (url: string, key: string, options: BaseOptions) => { const { configDirectory: configDir } = options; await connect(url, key); + await requirePermissions([Permission.UserRead]); const [error, user] = await withError(getMyUser()); if (error) { diff --git a/cli/src/commands/server-info.ts b/cli/src/commands/server-info.ts index bea49231c9..9a5098e628 100644 --- a/cli/src/commands/server-info.ts +++ b/cli/src/commands/server-info.ts @@ -1,8 +1,9 @@ -import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; -import { BaseOptions, authenticate } from 'src/utils'; +import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes, Permission } from '@immich/sdk'; +import { authenticate, BaseOptions, requirePermissions } from 'src/utils'; export const serverInfo = async (options: BaseOptions) => { const { url } = await authenticate(options); + await requirePermissions([Permission.ServerAbout, Permission.AssetStatistics, Permission.UserRead]); const [versionInfo, mediaTypes, stats, userInfo] = await Promise.all([ getServerVersion(), diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 9ef20b3679..38bd119459 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -1,4 +1,4 @@ -import { getMyUser, init, isHttpError } from '@immich/sdk'; +import { ApiKeyResponseDto, getMyApiKey, getMyUser, init, isHttpError, Permission } from '@immich/sdk'; import { convertPathToPattern, glob } from 'fast-glob'; import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; @@ -34,6 +34,36 @@ export const authenticate = async (options: BaseOptions): Promise => { return auth; }; +export const s = (count: number) => (count === 1 ? '' : 's'); + +let _apiKey: ApiKeyResponseDto; +export const requirePermissions = async (permissions: Permission[]) => { + if (!_apiKey) { + _apiKey = await getMyApiKey(); + } + + if (_apiKey.permissions.includes(Permission.All)) { + return; + } + + const missing: Permission[] = []; + + for (const permission of permissions) { + if (!_apiKey.permissions.includes(permission)) { + missing.push(permission); + } + } + + if (missing.length > 0) { + const combined = missing.map((permission) => `"${permission}"`).join(', '); + console.log( + `Missing required permission${s(missing.length)}: ${combined}. +Please make sure your API key has the correct permissions.`, + ); + process.exit(1); + } +}; + export const connect = async (url: string, key: string) => { const wellKnownUrl = new URL('.well-known/immich', url); try { diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 244fc74dba..81fc492001 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -127,7 +127,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63 + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e healthcheck: test: redis-cli ping || exit 1 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 217ec08030..4d9e7efbe9 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -56,7 +56,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63 + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/docker/docker-compose.rootless.yml b/docker/docker-compose.rootless.yml new file mode 100644 index 0000000000..f6eb38a429 --- /dev/null +++ b/docker/docker-compose.rootless.yml @@ -0,0 +1,100 @@ +# +# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose +# +# Make sure to use the docker-compose.yml of the current release: +# +# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml +# +# The compose file on main may not be compatible with the latest release. + +name: immich + +services: + immich-server: + container_name: immich_server + image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} + # extends: + # file: hwaccel.transcoding.yml + # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + volumes: + # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file + - ${UPLOAD_LOCATION}:/data + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + ports: + - '2283:2283' + depends_on: + - redis + - database + restart: always + healthcheck: + disable: false + + immich-machine-learning: + container_name: immich_machine_learning + # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag. + # Example tag: ${IMMICH_VERSION:-release}-cuda + image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} + # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration + # file: hwaccel.ml.yml + # service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + volumes: + - ./ml-model-cache:/cache + - ./ml-dotcache:/.cache + - ./ml-config:/.config + env_file: + - .env + restart: always + healthcheck: + disable: false + + redis: + container_name: immich_redis + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + volumes: + - ./redis:/data + healthcheck: + test: redis-cli ping || exit 1 + restart: always + + database: + container_name: immich_postgres + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23 + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_INITDB_ARGS: '--data-checksums' + # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs + # DB_STORAGE_TYPE: 'HDD' + volumes: + # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file + - ${DB_DATA_LOCATION}:/var/lib/postgresql/data + shm_size: 128mb + restart: always + healthcheck: + disable: false + +volumes: + model-cache: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b8668cc91a..3d92655453 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -49,7 +49,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63 + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 2fa8fd12b0..7b7a265ddf 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -402,6 +402,9 @@ To decrease Redis logs, you can add the following line to the `redis:` section o ### How can I run Immich as a non-root user? You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service. + +[Example docker-compose.yml file](https://github.com/immich-app/immich/blob/main/docker/docker-compose.rootless.yml) + You may need to add mount points or docker volumes for the following internal container paths: - `immich-machine-learning:/.config` diff --git a/docs/docs/developer/devcontainers.md b/docs/docs/developer/devcontainers.md index f50ec62d8a..c4f673de6e 100644 --- a/docs/docs/developer/devcontainers.md +++ b/docs/docs/developer/devcontainers.md @@ -408,7 +408,7 @@ If you encounter issues: 1. Check container logs: View → Output → Select "Dev Containers" 2. Rebuild without cache: "Dev Containers: Rebuild Container Without Cache" 3. Review [common Docker issues](https://docs.docker.com/desktop/troubleshoot/) -4. Ask in [Discord](https://discord.immich.app) `#help-desk-support` channel +4. Ask in [Discord](https://discord.immich.app) `#contributing` channel ## Mobile Development diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md index 685f23932c..059cf9a115 100644 --- a/docs/docs/features/ml-hardware-acceleration.md +++ b/docs/docs/features/ml-hardware-acceleration.md @@ -86,8 +86,8 @@ You do not need to redo any machine learning jobs after enabling hardware accele ## Setup 1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`. -2. In the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend. -3. Still in `immich-machine-learning`, add one of -[armnn, cuda, rocm, openvino, rknn] to the `image` section's tag at the end of the line. +2. In `immich-machine-learning`, add one of -[armnn, cuda, rocm, openvino, rknn] to the `image` section's tag at the end of the line. +3. Still in the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend. 4. Redeploy the `immich-machine-learning` container with these updated settings. ### Confirming Device Usage diff --git a/docs/src/pages/errors.md b/docs/src/pages/errors.md index fed72f21c7..6189fcaae1 100644 --- a/docs/src/pages/errors.md +++ b/docs/src/pages/errors.md @@ -32,3 +32,7 @@ If you would like to migrate from one media location to another, simply successf 4. Start up Immich After version `1.136.0`, Immich can detect when a media location has moved and will automatically update the database paths to keep them in sync. + +## Schema drift + +Schema drift is when the database schema is out of sync with the code. This could be the result of manual database tinkering, issues during a database restore, or something else. Schema drift can lead to data corruption, application bugs, and other unpredictable behavior. Please reconcile the differences as soon as possible. Specifically, missing `CONSTRAINT`s can result in duplicate assets being uploaded, since the server relies on a checksum `CONSTRAINT` to prevent duplicates. diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 795926ac89..564eeafa94 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,7 +1,7 @@ [ { - "label": "v2.5.5", - "url": "https://docs.v2.5.5.archive.immich.app" + "label": "v2.5.6", + "url": "https://docs.v2.5.6.archive.immich.app" }, { "label": "v2.4.1", diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index a98a7013a4..5a79396aa5 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -53,6 +53,7 @@ services: POSTGRES_DB: immich ports: - 5435:5432 + shm_size: 128mb healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres -d immich'] interval: 1s diff --git a/e2e/package.json b/e2e/package.json index 7271a65ffa..abe46a39ca 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "2.5.5", + "version": "2.5.6", "description": "", "main": "index.js", "type": "module", @@ -21,13 +21,13 @@ "devDependencies": { "@eslint/js": "^9.8.0", "@faker-js/faker": "^10.1.0", - "@immich/cli": "file:../cli", - "@immich/e2e-auth-server": "file:../e2e-auth-server", - "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/cli": "workspace:*", + "@immich/e2e-auth-server": "workspace:*", + "@immich/sdk": "workspace:*", "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^24.10.9", + "@types/node": "^24.10.11", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 58f5997343..6dd8c10d25 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -3,7 +3,7 @@ import dotenv from 'dotenv'; import { cpus } from 'node:os'; import { resolve } from 'node:path'; -dotenv.config({ path: resolve(import.meta.dirname, '.env') }); +dotenv.config({ quiet: true, path: resolve(import.meta.dirname, '.env') }); export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1'; export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1'; @@ -14,7 +14,8 @@ export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERV process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1'; const config: PlaywrightTestConfig = { - testDir: './src/web/specs', + testDir: './src/specs/server', + testMatch: /.*\.e2e-spec\.ts/, fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 4 : 0, @@ -28,54 +29,28 @@ const config: PlaywrightTestConfig = { }, }, - testMatch: /.*\.e2e-spec\.ts/, - workers: process.env.CI ? 4 : Math.round(cpus().length * 0.75), projects: [ { - name: 'chromium', + name: 'web', use: { ...devices['Desktop Chrome'] }, - testMatch: /.*\.e2e-spec\.ts/, + testDir: './src/specs/web', workers: 1, }, { name: 'ui', use: { ...devices['Desktop Chrome'] }, - testMatch: /.*\.ui-spec\.ts/, + testDir: './src/ui/specs', fullyParallel: true, workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1), }, - - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, + { + name: 'maintenance', + use: { ...devices['Desktop Chrome'] }, + testDir: './src/specs/maintenance', + workers: 1, + }, ], /* Run your local dev server before starting the tests */ diff --git a/e2e/src/setup/docker-compose.ts b/e2e/src/docker-compose.ts similarity index 100% rename from e2e/src/setup/docker-compose.ts rename to e2e/src/docker-compose.ts diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 9585484355..3d7971d6f0 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -43,10 +43,10 @@ export const errorDto = { message: 'Invalid share key', correlationId: expect.any(String), }, - invalidSharePassword: { + passwordRequired: { error: 'Unauthorized', statusCode: 401, - message: 'Invalid password', + message: 'Password required', correlationId: expect.any(String), }, badRequest: (message: any = null) => ({ diff --git a/e2e/src/web/specs/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/database-backups.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/database-backups.e2e-spec.ts rename to e2e/src/specs/maintenance/database-backups.e2e-spec.ts diff --git a/e2e/src/web/specs/maintenance.e2e-spec.ts b/e2e/src/specs/maintenance/maintenance.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/maintenance.e2e-spec.ts rename to e2e/src/specs/maintenance/maintenance.e2e-spec.ts diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/specs/server/api/activity.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/activity.e2e-spec.ts rename to e2e/src/specs/server/api/activity.e2e-spec.ts diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/album.e2e-spec.ts rename to e2e/src/specs/server/api/album.e2e-spec.ts diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/specs/server/api/api-key.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/api-key.e2e-spec.ts rename to e2e/src/specs/server/api/api-key.e2e-spec.ts diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/asset.e2e-spec.ts rename to e2e/src/specs/server/api/asset.e2e-spec.ts diff --git a/e2e/src/api/specs/database-backups.e2e-spec.ts b/e2e/src/specs/server/api/database-backups.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/database-backups.e2e-spec.ts rename to e2e/src/specs/server/api/database-backups.e2e-spec.ts diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/specs/server/api/download.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/download.e2e-spec.ts rename to e2e/src/specs/server/api/download.e2e-spec.ts diff --git a/e2e/src/api/specs/jobs.e2e-spec.ts b/e2e/src/specs/server/api/jobs.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/jobs.e2e-spec.ts rename to e2e/src/specs/server/api/jobs.e2e-spec.ts diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/specs/server/api/library.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/library.e2e-spec.ts rename to e2e/src/specs/server/api/library.e2e-spec.ts diff --git a/e2e/src/api/specs/maintenance.e2e-spec.ts b/e2e/src/specs/server/api/maintenance.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/maintenance.e2e-spec.ts rename to e2e/src/specs/server/api/maintenance.e2e-spec.ts diff --git a/e2e/src/api/specs/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/map.e2e-spec.ts rename to e2e/src/specs/server/api/map.e2e-spec.ts diff --git a/e2e/src/api/specs/memory.e2e-spec.ts b/e2e/src/specs/server/api/memory.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/memory.e2e-spec.ts rename to e2e/src/specs/server/api/memory.e2e-spec.ts diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/oauth.e2e-spec.ts rename to e2e/src/specs/server/api/oauth.e2e-spec.ts diff --git a/e2e/src/api/specs/partner.e2e-spec.ts b/e2e/src/specs/server/api/partner.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/partner.e2e-spec.ts rename to e2e/src/specs/server/api/partner.e2e-spec.ts diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/specs/server/api/person.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/person.e2e-spec.ts rename to e2e/src/specs/server/api/person.e2e-spec.ts diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/search.e2e-spec.ts rename to e2e/src/specs/server/api/search.e2e-spec.ts diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/server.e2e-spec.ts rename to e2e/src/specs/server/api/server.e2e-spec.ts diff --git a/e2e/src/api/specs/session.e2e-spec.ts b/e2e/src/specs/server/api/session.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/session.e2e-spec.ts rename to e2e/src/specs/server/api/session.e2e-spec.ts diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts similarity index 99% rename from e2e/src/api/specs/shared-link.e2e-spec.ts rename to e2e/src/specs/server/api/shared-link.e2e-spec.ts index 8c15a14da5..80232beb75 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -239,7 +239,7 @@ describe('/shared-links', () => { const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key }); expect(status).toBe(401); - expect(body).toEqual(errorDto.invalidSharePassword); + expect(body).toEqual(errorDto.passwordRequired); }); it('should get data for correct password protected link', async () => { diff --git a/e2e/src/api/specs/stack.e2e-spec.ts b/e2e/src/specs/server/api/stack.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/stack.e2e-spec.ts rename to e2e/src/specs/server/api/stack.e2e-spec.ts diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/specs/server/api/system-config.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/system-config.e2e-spec.ts rename to e2e/src/specs/server/api/system-config.e2e-spec.ts diff --git a/e2e/src/api/specs/system-metadata.e2e-spec.ts b/e2e/src/specs/server/api/system-metadata.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/system-metadata.e2e-spec.ts rename to e2e/src/specs/server/api/system-metadata.e2e-spec.ts diff --git a/e2e/src/api/specs/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/tag.e2e-spec.ts rename to e2e/src/specs/server/api/tag.e2e-spec.ts diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/specs/server/api/trash.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/trash.e2e-spec.ts rename to e2e/src/specs/server/api/trash.e2e-spec.ts diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/user-admin.e2e-spec.ts rename to e2e/src/specs/server/api/user-admin.e2e-spec.ts diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/user.e2e-spec.ts rename to e2e/src/specs/server/api/user.e2e-spec.ts diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/specs/server/cli/login.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/login.e2e-spec.ts rename to e2e/src/specs/server/cli/login.e2e-spec.ts diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/specs/server/cli/server-info.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/server-info.e2e-spec.ts rename to e2e/src/specs/server/cli/server-info.e2e-spec.ts diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/specs/server/cli/upload.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/upload.e2e-spec.ts rename to e2e/src/specs/server/cli/upload.e2e-spec.ts diff --git a/e2e/src/cli/specs/version.e2e-spec.ts b/e2e/src/specs/server/cli/version.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/version.e2e-spec.ts rename to e2e/src/specs/server/cli/version.e2e-spec.ts diff --git a/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts b/e2e/src/specs/server/immich-admin/immich-admin.e2e-spec.ts similarity index 100% rename from e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts rename to e2e/src/specs/server/immich-admin/immich-admin.e2e-spec.ts diff --git a/e2e/src/web/specs/album.e2e-spec.ts b/e2e/src/specs/web/album.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/album.e2e-spec.ts rename to e2e/src/specs/web/album.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/navbar.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/navbar.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/slideshow.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/slideshow.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/stack.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/stack.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/stack.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/stack.e2e-spec.ts diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/specs/web/auth.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/auth.e2e-spec.ts rename to e2e/src/specs/web/auth.e2e-spec.ts diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/photo-viewer.e2e-spec.ts rename to e2e/src/specs/web/photo-viewer.e2e-spec.ts diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/specs/web/shared-link.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/shared-link.e2e-spec.ts rename to e2e/src/specs/web/shared-link.e2e-spec.ts diff --git a/e2e/src/web/specs/user-admin.e2e-spec.ts b/e2e/src/specs/web/user-admin.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/user-admin.e2e-spec.ts rename to e2e/src/specs/web/user-admin.e2e-spec.ts diff --git a/e2e/src/web/specs/websocket.e2e-spec.ts b/e2e/src/specs/web/websocket.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/websocket.e2e-spec.ts rename to e2e/src/specs/web/websocket.e2e-spec.ts diff --git a/e2e/src/generators/memory.ts b/e2e/src/ui/generators/memory.ts similarity index 100% rename from e2e/src/generators/memory.ts rename to e2e/src/ui/generators/memory.ts diff --git a/e2e/src/generators/memory/model-objects.ts b/e2e/src/ui/generators/memory/model-objects.ts similarity index 89% rename from e2e/src/generators/memory/model-objects.ts rename to e2e/src/ui/generators/memory/model-objects.ts index 1bcc703ed8..f81b2f8896 100644 --- a/e2e/src/generators/memory/model-objects.ts +++ b/e2e/src/ui/generators/memory/model-objects.ts @@ -1,9 +1,9 @@ import { faker } from '@faker-js/faker'; import { MemoryType, type MemoryResponseDto, type OnThisDayDto } from '@immich/sdk'; import { DateTime } from 'luxon'; -import { toAssetResponseDto } from 'src/generators/timeline/rest-response'; -import type { MockTimelineAsset } from 'src/generators/timeline/timeline-config'; -import { SeededRandom, selectRandomMultiple } from 'src/generators/timeline/utils'; +import { toAssetResponseDto } from 'src/ui/generators/timeline/rest-response'; +import type { MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config'; +import { SeededRandom, selectRandomMultiple } from 'src/ui/generators/timeline/utils'; export type MemoryConfig = { id?: string; diff --git a/e2e/src/generators/timeline.ts b/e2e/src/ui/generators/timeline.ts similarity index 100% rename from e2e/src/generators/timeline.ts rename to e2e/src/ui/generators/timeline.ts diff --git a/e2e/src/generators/timeline/distribution-patterns.ts b/e2e/src/ui/generators/timeline/distribution-patterns.ts similarity index 98% rename from e2e/src/generators/timeline/distribution-patterns.ts rename to e2e/src/ui/generators/timeline/distribution-patterns.ts index ae621fd9c5..b6f3aab6de 100644 --- a/e2e/src/generators/timeline/distribution-patterns.ts +++ b/e2e/src/ui/generators/timeline/distribution-patterns.ts @@ -1,5 +1,5 @@ -import { generateConsecutiveDays, generateDayAssets } from 'src/generators/timeline/model-objects'; -import { SeededRandom, selectRandomDays } from 'src/generators/timeline/utils'; +import { generateConsecutiveDays, generateDayAssets } from 'src/ui/generators/timeline/model-objects'; +import { SeededRandom, selectRandomDays } from 'src/ui/generators/timeline/utils'; import type { MockTimelineAsset } from './timeline-config'; import { GENERATION_CONSTANTS } from './timeline-config'; diff --git a/e2e/src/generators/timeline/images.ts b/e2e/src/ui/generators/timeline/images.ts similarity index 98% rename from e2e/src/generators/timeline/images.ts rename to e2e/src/ui/generators/timeline/images.ts index 69ec576714..9330cf137d 100644 --- a/e2e/src/generators/timeline/images.ts +++ b/e2e/src/ui/generators/timeline/images.ts @@ -1,5 +1,5 @@ import sharp from 'sharp'; -import { SeededRandom } from 'src/generators/timeline/utils'; +import { SeededRandom } from 'src/ui/generators/timeline/utils'; export const randomThumbnail = async (seed: string, ratio: number) => { const height = 235; diff --git a/e2e/src/generators/timeline/model-objects.ts b/e2e/src/ui/generators/timeline/model-objects.ts similarity index 99% rename from e2e/src/generators/timeline/model-objects.ts rename to e2e/src/ui/generators/timeline/model-objects.ts index f06596fd1a..e300de1161 100644 --- a/e2e/src/generators/timeline/model-objects.ts +++ b/e2e/src/ui/generators/timeline/model-objects.ts @@ -6,7 +6,7 @@ import { faker } from '@faker-js/faker'; import { AssetVisibility } from '@immich/sdk'; import { DateTime } from 'luxon'; import { writeFileSync } from 'node:fs'; -import { SeededRandom } from 'src/generators/timeline/utils'; +import { SeededRandom } from 'src/ui/generators/timeline/utils'; import type { DayPattern, MonthDistribution } from './distribution-patterns'; import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns'; import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config'; diff --git a/e2e/src/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts similarity index 99% rename from e2e/src/generators/timeline/rest-response.ts rename to e2e/src/ui/generators/timeline/rest-response.ts index a193535cd3..0c4bd06dc3 100644 --- a/e2e/src/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -15,7 +15,7 @@ import { } from '@immich/sdk'; import { DateTime } from 'luxon'; import { signupDto } from 'src/fixtures'; -import { parseTimeBucketKey } from 'src/generators/timeline/utils'; +import { parseTimeBucketKey } from 'src/ui/generators/timeline/utils'; import type { MockTimelineAsset, MockTimelineData } from './timeline-config'; /** diff --git a/e2e/src/generators/timeline/timeline-config.ts b/e2e/src/ui/generators/timeline/timeline-config.ts similarity index 98% rename from e2e/src/generators/timeline/timeline-config.ts rename to e2e/src/ui/generators/timeline/timeline-config.ts index 8dbe8399b1..992480eef9 100644 --- a/e2e/src/generators/timeline/timeline-config.ts +++ b/e2e/src/ui/generators/timeline/timeline-config.ts @@ -1,5 +1,5 @@ import type { AssetVisibility } from '@immich/sdk'; -import { DayPattern, MonthDistribution } from 'src/generators/timeline/distribution-patterns'; +import { DayPattern, MonthDistribution } from 'src/ui/generators/timeline/distribution-patterns'; // Constants for generation parameters export const GENERATION_CONSTANTS = { diff --git a/e2e/src/generators/timeline/utils.ts b/e2e/src/ui/generators/timeline/utils.ts similarity index 98% rename from e2e/src/generators/timeline/utils.ts rename to e2e/src/ui/generators/timeline/utils.ts index 686a8223ef..283f56c6f0 100644 --- a/e2e/src/generators/timeline/utils.ts +++ b/e2e/src/ui/generators/timeline/utils.ts @@ -1,5 +1,5 @@ import { DateTime } from 'luxon'; -import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/generators/timeline/timeline-config'; +import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config'; /** * Linear Congruential Generator for deterministic pseudo-random numbers diff --git a/e2e/src/mock-network/base-network.ts b/e2e/src/ui/mock-network/base-network.ts similarity index 100% rename from e2e/src/mock-network/base-network.ts rename to e2e/src/ui/mock-network/base-network.ts diff --git a/e2e/src/mock-network/memory-network.ts b/e2e/src/ui/mock-network/memory-network.ts similarity index 100% rename from e2e/src/mock-network/memory-network.ts rename to e2e/src/ui/mock-network/memory-network.ts diff --git a/e2e/src/mock-network/timeline-network.ts b/e2e/src/ui/mock-network/timeline-network.ts similarity index 98% rename from e2e/src/mock-network/timeline-network.ts rename to e2e/src/ui/mock-network/timeline-network.ts index 8780409657..b20a812eb1 100644 --- a/e2e/src/mock-network/timeline-network.ts +++ b/e2e/src/ui/mock-network/timeline-network.ts @@ -10,8 +10,8 @@ import { randomPreview, randomThumbnail, TimelineData, -} from 'src/generators/timeline'; -import { sleep } from 'src/web/specs/timeline/utils'; +} from 'src/ui/generators/timeline'; +import { sleep } from 'src/ui/specs/timeline/utils'; export class TimelineTestContext { slowBucket = false; diff --git a/e2e/src/web/specs/asset-viewer/asset-viewer.ui-spec.ts b/e2e/src/ui/specs/asset-viewer/asset-viewer.e2e-spec.ts similarity index 98% rename from e2e/src/web/specs/asset-viewer/asset-viewer.ui-spec.ts rename to e2e/src/ui/specs/asset-viewer/asset-viewer.e2e-spec.ts index 669f1b815c..082ff1f7a1 100644 --- a/e2e/src/web/specs/asset-viewer/asset-viewer.ui-spec.ts +++ b/e2e/src/ui/specs/asset-viewer/asset-viewer.e2e-spec.ts @@ -8,11 +8,11 @@ import { selectRandom, TimelineAssetConfig, TimelineData, -} from 'src/generators/timeline'; -import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; -import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network'; import { utils } from 'src/utils'; -import { assetViewerUtils } from 'src/web/specs/timeline/utils'; +import { assetViewerUtils } from '../timeline/utils'; test.describe.configure({ mode: 'parallel' }); test.describe('asset-viewer', () => { diff --git a/e2e/src/web/specs/memory/memory-viewer.ui-spec.ts b/e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts similarity index 96% rename from e2e/src/web/specs/memory/memory-viewer.ui-spec.ts rename to e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts index 11e73fbe25..dbc21ad1c9 100644 --- a/e2e/src/web/specs/memory/memory-viewer.ui-spec.ts +++ b/e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts @@ -1,18 +1,18 @@ import { faker } from '@faker-js/faker'; import type { MemoryResponseDto } from '@immich/sdk'; import { test } from '@playwright/test'; -import { generateMemoriesFromTimeline } from 'src/generators/memory'; +import { generateMemoriesFromTimeline } from 'src/ui/generators/memory'; import { Changes, createDefaultTimelineConfig, generateTimelineData, TimelineAssetConfig, TimelineData, -} from 'src/generators/timeline'; -import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; -import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/mock-network/memory-network'; -import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; -import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from 'src/web/specs/memory/utils'; +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; +import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/ui/mock-network/memory-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network'; +import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from './utils'; test.describe.configure({ mode: 'parallel' }); diff --git a/e2e/src/web/specs/memory/utils.ts b/e2e/src/ui/specs/memory/utils.ts similarity index 100% rename from e2e/src/web/specs/memory/utils.ts rename to e2e/src/ui/specs/memory/utils.ts diff --git a/e2e/src/web/specs/search/search-gallery.ui-spec.ts b/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts similarity index 95% rename from e2e/src/web/specs/search/search-gallery.ui-spec.ts rename to e2e/src/ui/specs/search/search-gallery.e2e-spec.ts index e358bed154..c3721b1c54 100644 --- a/e2e/src/web/specs/search/search-gallery.ui-spec.ts +++ b/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts @@ -6,10 +6,10 @@ import { generateTimelineData, TimelineAssetConfig, TimelineData, -} from 'src/generators/timeline'; -import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; -import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; -import { assetViewerUtils } from 'src/web/specs/timeline/utils'; +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network'; +import { assetViewerUtils } from '../timeline/utils'; const buildSearchUrl = (assetId: string) => { const searchQuery = encodeURIComponent(JSON.stringify({ originalFileName: 'test' })); diff --git a/e2e/src/web/specs/timeline/timeline.ui-spec.ts b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts similarity index 99% rename from e2e/src/web/specs/timeline/timeline.ui-spec.ts rename to e2e/src/ui/specs/timeline/timeline.e2e-spec.ts index 47026e2cd4..9408f6079a 100644 --- a/e2e/src/web/specs/timeline/timeline.ui-spec.ts +++ b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts @@ -12,18 +12,15 @@ import { selectRandomMultiple, TimelineAssetConfig, TimelineData, -} from 'src/generators/timeline'; -import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; -import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; -import { utils } from 'src/utils'; +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; import { - assetViewerUtils, - padYearMonth, - pageUtils, - poll, - thumbnailUtils, - timelineUtils, -} from 'src/web/specs/timeline/utils'; + pageRoutePromise, + setupTimelineMockApiRoutes, + TimelineTestContext, +} from 'src/ui/mock-network/timeline-network'; +import { utils } from 'src/utils'; +import { assetViewerUtils, padYearMonth, pageUtils, poll, thumbnailUtils, timelineUtils } from './utils'; test.describe.configure({ mode: 'parallel' }); test.describe('Timeline', () => { diff --git a/e2e/src/web/specs/timeline/utils.ts b/e2e/src/ui/specs/timeline/utils.ts similarity index 99% rename from e2e/src/web/specs/timeline/utils.ts rename to e2e/src/ui/specs/timeline/utils.ts index 0f04bf9361..774839b174 100644 --- a/e2e/src/web/specs/timeline/utils.ts +++ b/e2e/src/ui/specs/timeline/utils.ts @@ -1,6 +1,6 @@ import { BrowserContext, expect, Page } from '@playwright/test'; import { DateTime } from 'luxon'; -import { TimelineAssetConfig } from 'src/generators/timeline'; +import { TimelineAssetConfig } from 'src/ui/generators/timeline'; export const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 1c7388b6ec..bfad377089 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -15,7 +15,6 @@ "incremental": true, "skipLibCheck": true, "esModuleInterop": true, - "rootDirs": ["src"], "baseUrl": "./" }, "include": ["src/**/*.ts"], diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index 48433eb830..953273a930 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -3,14 +3,14 @@ import { defineConfig } from 'vitest/config'; // skip `docker compose up` if `make e2e` was already run const globalSetup: string[] = []; try { - await fetch('http://127.0.0.1:2285/api/server-info/ping'); + await fetch('http://127.0.0.1:2285/api/server/ping'); } catch { - globalSetup.push('src/setup/docker-compose.ts'); + globalSetup.push('src/docker-compose.ts'); } export default defineConfig({ test: { - include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'], + include: ['src/specs/server/**/*.e2e-spec.ts'], globalSetup, testTimeout: 15_000, pool: 'threads', diff --git a/i18n/ar.json b/i18n/ar.json index 6702d4c695..a1c29402c2 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -782,6 +782,8 @@ "client_cert_import": "استيراد", "client_cert_import_success_msg": "تم استيراد شهادة العميل", "client_cert_invalid_msg": "ملف شهادة عميل غير صالحة او كلمة سر غير صحيحة", + "client_cert_password_message": "أدخل كلمة المرور الخاصة بهذه الشهادة", + "client_cert_password_title": "كلمة المرور الخاصة بالشهادة", "client_cert_remove_msg": "تم ازالة شهادة العميل", "client_cert_subtitle": "يدعم صيغ PKCS12 (.p12, .pfx)فقط. استيراد/ازالة الشهادات متاح فقط قبل تسجيل الدخول", "client_cert_title": "شهادة مستخدم SSL [تجريبية]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "لن يتم حفظ التغييرات", "editor_close_without_save_title": "إغلاق المحرر؟", "editor_confirm_reset_all_changes": "هل أنت متأكد من إعادة ضبط جميع التغييرات؟", + "editor_discard_edits_confirm": "تجاهل التعديلات", + "editor_discard_edits_prompt": "لديك تعديلات غير محفوظة. هل أنت متأكد من رغبتك في تجاهلها؟", + "editor_discard_edits_title": "تجاهل التعديلات؟", + "editor_edits_applied_error": "فشل تطبيق التعديلات", + "editor_edits_applied_success": "تم تطبيق التعديلات بنجاح", "editor_flip_horizontal": "اقلب أفقيًا", "editor_flip_vertical": "اقلب عموديًا", "editor_orientation": "اتجاه", @@ -1196,6 +1203,8 @@ "features_in_development": "الميزات قيد التطوير", "features_setting_description": "إدارة ميزات التطبيق", "file_name_or_extension": "اسم الملف أو امتداده", + "file_name_text": "أسم الملف", + "file_name_with_value": "اسم الملف: {file_name}", "file_size": "حجم الملف", "filename": "اسم الملف", "filetype": "نوع الملف", @@ -1604,7 +1613,6 @@ "not_available": "غير متاح", "not_in_any_album": "ليست في أي ألبوم", "not_selected": "لم يختار", - "note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق سمة التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل", "notes": "ملاحظات", "nothing_here_yet": "لا يوجد شيء هنا بعد", "notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "تمت إعادة تعيين {count, plural, one {# المحتوى} other {# المحتويات}} إلى شخص جديد", "reassing_hint": "تعيين المحتويات المحددة لشخص موجود", "recent": "حديث", - "recent-albums": "ألبومات الحديثة", + "recent_albums": "ألبومات الحديثة", "recent_searches": "عمليات البحث الأخيرة", "recently_added": "اضيف مؤخرا", "recently_added_page_title": "أضيف مؤخرا", diff --git a/i18n/be.json b/i18n/be.json index 13ac6747f1..1c446c0cbd 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -457,7 +457,7 @@ "reassign": "Перапрызначыць", "reassing_hint": "Прыпісаць выбраныя актывы існуючай асобе", "recent": "Нядаўні", - "recent-albums": "Нядаўнія альбомы", + "recent_albums": "Нядаўнія альбомы", "recent_searches": "Нядаўнія пошукі", "recently_added": "Нядаўна дададзена", "refresh_faces": "Абнавіць твары", diff --git a/i18n/bg.json b/i18n/bg.json index c1cf0abdf5..0d39878cad 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -272,7 +272,7 @@ "oauth_auto_register": "Автоматична регистрация", "oauth_auto_register_description": "Автоматично регистриране на нови потребители след влизане с OAuth", "oauth_button_text": "Текст на бутона", - "oauth_client_secret_description": "Задължително за поверителен клиент или когато PKCE (Proof Key for Code Exchange) не се поддържа за публичен клиент.", + "oauth_client_secret_description": "Задължително за поверителен клиент или когато не се поддържа PKCE (Proof Key for Code Exchange) за публичен клиент.", "oauth_enable_description": "Влизане с OAuth", "oauth_mobile_redirect_uri": "URI за мобилно пренасочване", "oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства", @@ -572,7 +572,7 @@ "asset_list_layout_sub_title": "Разположение", "asset_list_settings_subtitle": "Настройки на мрежата на разполагане на снимки", "asset_list_settings_title": "Разполагане на снимки", - "asset_not_found_on_device_android": "Активът не е намерен на устройството", + "asset_not_found_on_device_android": "Обектът не е намерен на устройството", "asset_not_found_on_device_ios": "Обектът не е намерен на устройството. Ако използвате iCloud, обектът може да е недостъпен поради повреден файл, съхранен в iCloud", "asset_not_found_on_icloud": "Обектът не е намерен в iCloud. Обектът може да е недостъпен поради повреден файл, съхранен в iCloud", "asset_offline": "Елементът е офлайн", @@ -782,6 +782,8 @@ "client_cert_import": "Импорт", "client_cert_import_success_msg": "Клиентския сертификат е импортиран", "client_cert_invalid_msg": "Невалиден сертификат или грешна парола", + "client_cert_password_message": "Въведете парола за този сертификат", + "client_cert_password_title": "Парола за сертификат", "client_cert_remove_msg": "Клиентския сертификат е премахнат", "client_cert_subtitle": "Поддържа се само формат PKCS12 (.p12, .pfx). Импорт/премахване на сертификат може само преди вписване в системата", "client_cert_title": "Клиентски SSL сертификат [ЕКСПЕРИМЕНТАЛНО]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "Промените няма да бъдат запазени", "editor_close_without_save_title": "Затваряне на редактора?", "editor_confirm_reset_all_changes": "Сигурни ли сте, че искате да възстановите всички промени?", + "editor_discard_edits_confirm": "Отхвърли промените", + "editor_discard_edits_prompt": "Имате незапазени промени. Наистина ли искате да ги отхвърлите?", + "editor_discard_edits_title": "Отхвърляме ли промените?", + "editor_edits_applied_error": "Неуспешно прилагане на промените", + "editor_edits_applied_success": "Успешно прилагане на промените", "editor_flip_horizontal": "Обърни хоризонтално", "editor_flip_vertical": "Обърни вертикално", "editor_orientation": "Ориентация", @@ -1196,6 +1203,8 @@ "features_in_development": "Функции в процес на разработка", "features_setting_description": "Управление на функциите на приложението", "file_name_or_extension": "Име на файл или разширение", + "file_name_text": "Имe на файл", + "file_name_with_value": "Име на файл: {file_name}", "file_size": "Размер на файла", "filename": "Име на файл", "filetype": "Тип на файл", @@ -1604,7 +1613,6 @@ "not_available": "Неналично", "not_in_any_album": "Не е в никой албум", "not_selected": "Не е избрано", - "note_apply_storage_label_to_previously_uploaded assets": "Забележка: За да приложите етикета за съхранение към предварително качени активи, стартирайте", "notes": "Бележки", "nothing_here_yet": "Засега тук няма нищо", "notification_permission_dialog_content": "За да включиш известията, отиди в Настройки и избери Разреши.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "Преназначени {count, plural, one {# елемент} other {# елемента}} на нов човек", "reassing_hint": "Назначи избраните елементи на съществуващо лице", "recent": "Скорошни", - "recent-albums": "Скорошни Албуми", + "recent_albums": "Скорошни Албуми", "recent_searches": "Скорошни търсения", "recently_added": "Наскоро добавено", "recently_added_page_title": "Наскоро добавено", @@ -2072,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} години", "shared_link_edit_password_hint": "Въведи парола за достъп до споделен ресурс", "shared_link_edit_submit_button": "Обнови връзката", - "shared_link_error_server_url_fetch": "Не може да се извлече URL адресът на сървъра", + "shared_link_error_server_url_fetch": "Не може да се извлече url-адресът на сървъра", "shared_link_expires_day": "Изтича след {count} ден", "shared_link_expires_days": "Изтича след {count} дни", "shared_link_expires_hour": "Изтича след {count} час", diff --git a/i18n/bi.json b/i18n/bi.json index c5c9edbbb1..290b816cc6 100644 --- a/i18n/bi.json +++ b/i18n/bi.json @@ -17,7 +17,7 @@ "readonly_mode_enabled": "Mod blo yu no save janjem i on", "reassigned_assets_to_new_person": "Janjem{count, plural, one {# asset} other {# assets}} blo nu man", "reassing_hint": "janjem ol sumtin yu bin joos i go blo wan man", - "recent-albums": "album i no old tu mas", + "recent_albums": "album i no old tu mas", "recent_searches": "lukabout wea i no old tu mas", "time_based_memories_duration": "hao mus second blo wan wan imij i stap lo scrin.", "timezone": "taemzon", diff --git a/i18n/bn.json b/i18n/bn.json index 2ed2b39338..7ba6c0a467 100644 --- a/i18n/bn.json +++ b/i18n/bn.json @@ -325,7 +325,16 @@ "storage_template_user_label": "{label} হলো ব্যবহারকারীর স্টোরেজ লেবেল (Storage Label)", "theme_settings_description": "ইমিচ (Immich) ওয়েব ইন্টারফেসের কাস্টমাইজেশন ম্যানেজ করুন", "thumbnail_generation_job": "থাম্বনেইল তৈরি করুন (Generate Thumbnails)", - "thumbnail_generation_job_description": "প্রতিটি অ্যাসেটের জন্য বড়, ছোট এবং ব্লার (অস্পষ্ট) থাম্বনেইল তৈরি করুন, সেই সাথে প্রতিটি ব্যক্তির জন্যও থাম্বনেইল তৈরি করুন।" + "thumbnail_generation_job_description": "প্রতিটি অ্যাসেটের জন্য বড়, ছোট এবং ব্লার (অস্পষ্ট) থাম্বনেইল তৈরি করুন, সেই সাথে প্রতিটি ব্যক্তির জন্যও থাম্বনেইল তৈরি করুন।", + "transcoding_acceleration_api": "অ্যাক্সিলারেট এপিআই (Acceleration API)", + "transcoding_acceleration_api_description": "ট্রানসকোডিং (transcoding) দ্রুত করার জন্য আপনার ডিভাইসের সাথে যে API ইন্টারঅ্যাক্ট করবে। এই সেটিংসটি 'সাধ্যমতো' (best effort) কাজ করবে: ব্যর্থ হলে এটি পুনরায় সফটওয়্যার ট্রানসকোডিংয়ে ফিরে আসবে। হার্ডওয়্যারের ওপর ভিত্তি করে VP9 কাজ করতেও পারে, আবার নাও করতে পারে।", + "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU প্রয়োজন)", + "transcoding_acceleration_qsv": "Quick Sync (৭ম প্রজন্মের ইনটেল CPU বা পরবর্তী ভার্সন প্রয়োজন)", + "transcoding_acceleration_rkmpp": "RKMPP (শুধুমাত্র Rockchip SOC-এর জন্য)", + "transcoding_acceleration_vaapi": "VA-API (ভিডিও অ্যাক্সিলারেশন এপিআই)", + "transcoding_accepted_audio_codecs": "গ্রহণযোগ্য অডিও কোডেকসমূহ (Accepted audio codecs)", + "transcoding_accepted_audio_codecs_description": "কোন অডিও কোডেকগুলো ট্রানসকোড করার প্রয়োজন নেই তা নির্বাচন করুন। এটি শুধুমাত্র নির্দিষ্ট ট্রানসকোড পলিসির (transcode policies) জন্য ব্যবহৃত হয়।", + "transcoding_accepted_containers": "গ্রহণযোগ্য কন্টেইনারসমূহ (Accepted containers)" }, "yes": "হ্যাঁ", "you_dont_have_any_shared_links": "আপনার কোনো শেয়ার করা লিঙ্ক নেই (You don't have any shared links)", diff --git a/i18n/ca.json b/i18n/ca.json index 737bb5bce8..563d5f15c5 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -311,7 +311,7 @@ "search_jobs": "Cercar treballs…", "send_welcome_email": "Enviar correu electrònic de benvinguda", "server_external_domain_settings": "Domini extern", - "server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://", + "server_external_domain_settings_description": "Domini utilitzat per a enllaços externs", "server_public_users": "Usuaris públics", "server_public_users_description": "Tots els usuaris (nom i correu electrònic) apareixen a la llista a l'afegir un usuari als àlbums compartits. Si es desactiva, la llista només serà disponible pels usuaris administradors.", "server_settings": "Configuració del servidor", @@ -782,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "S'ha importat el certificat del client", "client_cert_invalid_msg": "Fitxer de certificat no vàlid o contrasenya incorrecta", + "client_cert_password_message": "Introdueix la contrasenya per a aquest certificat", + "client_cert_password_title": "Contrasenya del certificat", "client_cert_remove_msg": "S'ha eliminat el certificat del client", "client_cert_subtitle": "Només admet el format PKCS12 (.p12, .pfx). La importació/eliminació de certificats només està disponible abans d'iniciar sessió", "client_cert_title": "Certificat de client SSL", @@ -792,6 +794,11 @@ "color": "Color", "color_theme": "Tema de color", "command": "Ordre", + "command_palette_prompt": "Trobar ràpidament pàgines, accions o comandes", + "command_palette_to_close": "per a tancar", + "command_palette_to_navigate": "per a introduir", + "command_palette_to_select": "per a seleccionar", + "command_palette_to_show_all": "per a mostrar-ho tot", "comment_deleted": "Comentari esborrat", "comment_options": "Opcions de comentari", "comments_and_likes": "Comentaris i agradaments", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "No es desaran els canvis", "editor_close_without_save_title": "Tancar l'editor?", "editor_confirm_reset_all_changes": "Segur que vols reiniciar tots els canvis?", + "editor_discard_edits_confirm": "Descarta les modificacions", + "editor_discard_edits_prompt": "Tens modificacions sense desar. Estàs segur que les vols descartar?", + "editor_discard_edits_title": "Vols descartar les modificacions?", + "editor_edits_applied_error": "No s'han pogut aplicar les modificacions", + "editor_edits_applied_success": "Les modificacions s'han aplicat correctament", "editor_flip_horizontal": "Capgira horitzontalment", "editor_flip_vertical": "Capgira verticalment", "editor_orientation": "Orientació", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONES", "exif_bottom_sheet_person_add_person": "Afegir nom", "exit_slideshow": "Surt de la presentació de diapositives", + "expand": "Ampliar-ho", "expand_all": "Ampliar-ho tot", "experimental_settings_new_asset_list_subtitle": "Treball en curs", "experimental_settings_new_asset_list_title": "Habilita la graella de fotos experimental", @@ -1196,6 +1209,8 @@ "features_in_development": "Funcions en desenvolupament", "features_setting_description": "Administrar les funcions de l'aplicació", "file_name_or_extension": "Nom de l'arxiu o extensió", + "file_name_text": "Nom del fitxer", + "file_name_with_value": "Nom del fitxer: {file_name}", "file_size": "Mida del fitxer", "filename": "Nom del fitxer", "filetype": "Tipus d'arxiu", @@ -1523,7 +1538,7 @@ "mobile_app_download_onboarding_note": "Descarregar la App de mòbil fent servir les seguents opcions", "model": "Model", "month": "Mes", - "monthly_title_text_date_format": "MMMM y", + "monthly_title_text_date_format": "MMMM a", "more": "Més", "move": "Moure", "move_down": "Moure cap avall", @@ -1604,7 +1619,6 @@ "not_available": "N/A", "not_in_any_album": "En cap àlbum", "not_selected": "No seleccionat", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el", "notes": "Notes", "nothing_here_yet": "No hi ha res encara", "notification_permission_dialog_content": "Per activar les notificacions, aneu a Configuració i seleccioneu permet.", @@ -1634,6 +1648,7 @@ "online": "En línia", "only_favorites": "Només preferits", "open": "Obrir", + "open_calendar": "Obrir el calendari", "open_in_map_view": "Obrir a la vista del mapa", "open_in_openstreetmap": "Obre a OpenStreetMap", "open_the_search_filters": "Obriu els filtres de cerca", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova", "reassing_hint": "Assignar els elements seleccionats a una persona existent", "recent": "Recent", - "recent-albums": "Àlbums recents", + "recent_albums": "Àlbums recents", "recent_searches": "Cerques recents", "recently_added": "Afegit recentment", "recently_added_page_title": "Afegit recentment", @@ -2175,6 +2190,7 @@ "support": "Suport", "support_and_feedback": "Suport i comentaris", "support_third_party_description": "La vostra instal·lació immich la va empaquetar un tercer. Els problemes que experimenteu poden ser causats per aquest paquet així que, si us plau, plantegeu els poblemes amb ells en primer lloc mitjançant els enllaços següents.", + "supporter": "Contribuïdor", "swap_merge_direction": "Canvia la direcció d'unió", "sync": "Sincronitza", "sync_albums": "Sincronitzar àlbums", diff --git a/i18n/cs.json b/i18n/cs.json index f72b9b164c..77da129f83 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -311,7 +311,7 @@ "search_jobs": "Hledat úlohy…", "send_welcome_email": "Odeslat uvítací e-mail", "server_external_domain_settings": "Externí doména", - "server_external_domain_settings_description": "Doména pro veřejně sdílené odkazy, včetně http(s)://", + "server_external_domain_settings_description": "Doména používaná pro externí odkazy", "server_public_users": "Veřejní uživatelé", "server_public_users_description": "Všichni uživatelé (jméno a e-mail) jsou uvedeni při přidávání uživatele do sdílených alb. Pokud je tato funkce vypnuta, bude seznam uživatelů dostupný pouze uživatelům z řad správců.", "server_settings": "Server", @@ -782,6 +782,8 @@ "client_cert_import": "Importovat", "client_cert_import_success_msg": "Klientský certifikát je importován", "client_cert_invalid_msg": "Neplatný soubor certifikátu nebo špatné heslo", + "client_cert_password_message": "Zadejte heslo pro tento certifikát", + "client_cert_password_title": "Heslo certifikátu", "client_cert_remove_msg": "Klientský certifikát je odstraněn", "client_cert_subtitle": "Podporuje pouze formát PKCS12 (.p12, .pfx). Import/odstranění certifikátu je možné pouze před přihlášením", "client_cert_title": "Klientský SSL certifikát [EXPERIMENTÁLNÍ]", @@ -792,6 +794,11 @@ "color": "Barva", "color_theme": "Barevný motiv", "command": "Příkaz", + "command_palette_prompt": "Rychlé vyhledávání stránek, akcí nebo příkazů", + "command_palette_to_close": "zavřít", + "command_palette_to_navigate": "vstoupit", + "command_palette_to_select": "vybrat", + "command_palette_to_show_all": "zobrazit vše", "comment_deleted": "Komentář odstraněn", "comment_options": "Možnosti komentáře", "comments_and_likes": "Komentáře a lajky", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "Změny nebudou uloženy", "editor_close_without_save_title": "Zavřít editor?", "editor_confirm_reset_all_changes": "Opravdu chcete zrušit všechny změny?", + "editor_discard_edits_confirm": "Zrušit úpravy", + "editor_discard_edits_prompt": "Máte neuložené úpravy. Opravdu je chcete smazat?", + "editor_discard_edits_title": "Zrušit úpravy?", + "editor_edits_applied_error": "Nepodařilo se použít úpravy", + "editor_edits_applied_success": "Úpravy byly úspěšně provedeny", "editor_flip_horizontal": "Otočit vodorovně", "editor_flip_vertical": "Otočit svisle", "editor_orientation": "Orientace", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "LIDÉ", "exif_bottom_sheet_person_add_person": "Přidat jméno", "exit_slideshow": "Ukončit prezentaci", + "expand": "Rozbalit", "expand_all": "Rozbalit vše", "experimental_settings_new_asset_list_subtitle": "Zpracovávám", "experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií", @@ -1196,6 +1209,8 @@ "features_in_development": "Funkce ve vývoji", "features_setting_description": "Správa funkcí aplikace", "file_name_or_extension": "Název nebo přípona souboru", + "file_name_text": "Název souboru", + "file_name_with_value": "Název souboru: {file_name}", "file_size": "Velikost souboru", "filename": "Název souboru", "filetype": "Typ souboru", @@ -1604,7 +1619,6 @@ "not_available": "Není k dispozici", "not_in_any_album": "Bez alba", "not_selected": "Není vybráno", - "note_apply_storage_label_to_previously_uploaded assets": "Upozornění: Chcete-li použít štítek úložiště na dříve nahrané položky, spusťte příkaz", "notes": "Poznámky", "nothing_here_yet": "Zatím zde nic není", "notification_permission_dialog_content": "Chcete-li povolit oznámení, přejděte do nastavení a vyberte možnost povolit.", @@ -1634,6 +1648,7 @@ "online": "Online", "only_favorites": "Pouze oblíbené", "open": "Otevřít", + "open_calendar": "Otevřít kalendář", "open_in_map_view": "Otevřít v zobrazení mapy", "open_in_openstreetmap": "Otevřít v OpenStreetMap", "open_the_search_filters": "Otevřít vyhledávací filtry", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {Přeřazena # položka} few {Přeřazeny # položky} other {Přeřazeno # položek}} na novou osobu", "reassing_hint": "Přiřazení vybraných položek existující osobě", "recent": "Nedávné", - "recent-albums": "Nedávná alba", + "recent_albums": "Nedávná alba", "recent_searches": "Nedávná vyhledávání", "recently_added": "Nedávno přidané", "recently_added_page_title": "Nedávno přidané", @@ -2175,6 +2190,7 @@ "support": "Podpora", "support_and_feedback": "Podpora a zpětná vazba", "support_third_party_description": "Vaše Immich instalace byla připravena třetí stranou. Problémy, které se u vás vyskytly, mohou být způsobeny tímto balíčkem, proto se na ně obraťte v první řadě pomocí níže uvedených odkazů.", + "supporter": "Podporovatel", "swap_merge_direction": "Obrátit směr sloučení", "sync": "Synchronizovat", "sync_albums": "Synchronizovat alba", diff --git a/i18n/da.json b/i18n/da.json index 7f2b77dc28..6981d6dae3 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -201,7 +201,7 @@ "maintenance_restore_database_backup_description": "Gendan en tidligere databasetilstand ved hjælp af en sikkerhedskopifil", "maintenance_settings": "Vedligeholdelse", "maintenance_settings_description": "Sæt Immich i vedligeholdelsestilstand.", - "maintenance_start": "Start vedligeholdelsestilstand", + "maintenance_start": "Skift til vedligeholdelsestilstand", "maintenance_start_error": "Vedligeholdelsestilstand kunne ikke startes.", "maintenance_upload_backup": "Upload databasebackupfil", "maintenance_upload_backup_error": "Kunne ikke uploade backup, er det en .sql/.sql.gz fil?", @@ -272,7 +272,7 @@ "oauth_auto_register": "Autoregistrér", "oauth_auto_register_description": "Registrér automatisk nye brugere efter at have logget ind med OAuth", "oauth_button_text": "Knaptekst", - "oauth_client_secret_description": "Påkrævet hvis PKCE (Proof Key for Code Exchange) ikke er supporteret af OAuth-udbyderen", + "oauth_client_secret_description": "Påkrævet for en fortrolig klient eller hvis PKCE (Proof Key for Code Exchange) ikke understøttes for en offentlig klient.", "oauth_enable_description": "Log ind med OAuth", "oauth_mobile_redirect_uri": "Mobilomdiregerings-URL", "oauth_mobile_redirect_uri_override": "Tilsidesættelse af mobil omdiregerings-URL", @@ -383,7 +383,7 @@ "transcoding_hardware_acceleration": "Hardwareacceleration", "transcoding_hardware_acceleration_description": "Eksperimentel: hurtigere transkodning men kan sænke kvaliteten ved samme bitrate", "transcoding_hardware_decoding": "Hardware-afkodning", - "transcoding_hardware_decoding_setting_description": "Gælder kun NVENC, QSV og RKMPP. Slår ende-til-ende acceleration til i stedet for kun at accelerere indkodning. Virker måske ikke på alle videoer.", + "transcoding_hardware_decoding_setting_description": "Slår ende‑til‑ende‑acceleration til i stedet for kun at accelerere indkodning. Virker muligvis ikke på alle videoer.", "transcoding_max_b_frames": "Maksimum B-frames", "transcoding_max_b_frames_description": "Højere værdier forbedrer kompressionseffektivitet, men kan gøre indkodning langsommere. Er måske ikke kompatibelt med hardware-acceleration på ældre enheder. 0 slår B-frames fra, mens -1 sætter denne værdi automatisk.", "transcoding_max_bitrate": "Maksimal bitrate", @@ -537,7 +537,7 @@ "app_bar_signout_dialog_content": "Er du sikker på, du vil logge ud?", "app_bar_signout_dialog_ok": "Ja", "app_bar_signout_dialog_title": "Log ud", - "app_download_links": "App Download Links", + "app_download_links": "Links til app download", "app_settings": "Appindstillinger", "app_stores": "App Butikker", "app_update_available": "App opdatering er tilgængelig", @@ -572,6 +572,9 @@ "asset_list_layout_sub_title": "Udseende", "asset_list_settings_subtitle": "Indstillinger for billedgitterlayout", "asset_list_settings_title": "Billedgitter", + "asset_not_found_on_device_android": "Kan ikke finde elementet på enheden", + "asset_not_found_on_device_ios": "Mediet blev ikke fundet på enheden. Hvis du bruger iCloud, kan mediet være utilgængeligt, hvis en fejlagtig fil ligger på iCloud", + "asset_not_found_on_icloud": "Mediet blev ikke fundet på iCloud. Det kan være utilgængeligt, hvis det er en fejlagtig fil, der ligger på iCloud", "asset_offline": "Mediefil offline", "asset_offline_description": "Denne eksterne mediefil kan ikke længere findes på drevet. Kontakt venligst din Immich-administrator for hjælp.", "asset_restored_successfully": "Elementet blev gendannet succesfuldt", @@ -763,10 +766,10 @@ "cleanup_found_assets": "Fandt {count} sikkerhedskopierede filer", "cleanup_found_assets_with_size": "Fundet {count} sikkerhedskopierede objekter ({size})", "cleanup_icloud_shared_albums_excluded": "iCloud delte albummer er udelukket fra scanningen", - "cleanup_no_assets_found": "Ingen sikkerhedskopierede filer fundet der matcher dine kriterier", + "cleanup_no_assets_found": "Ingen elementer matcher kriterierne ovenfor. Frigiv plads kan kun fjerne elementer, der er sikkerhedskopieret til serveren", "cleanup_preview_title": "Filer at fjerne ({count})", - "cleanup_step3_description": "Scan efter fotos og videoer, der er blevet sikkerhedskopieret til serveren med den valgte stop-dato og filtermuligheder", - "cleanup_step4_summary": "{count} filer lavet før {date} er i kø for at blive fjernet fra denne enhed", + "cleanup_step3_description": "Skan efter sikkerhedskopierede elementer, som matcher dine dato- og indstillingsvalg.", + "cleanup_step4_summary": "{count, plural, one {element} other {elementer}} oprettet før {date} står til at blive fjernet fra denne enhed. Billeder vil stadig være tilgængelige i Immich‑appen.", "cleanup_trash_hint": "For at genvinde lagringsplads helt, skal du åbne din indbyggede galleriapp og tømme papirkurven", "clear": "Ryd", "clear_all": "Ryd alle", @@ -779,6 +782,8 @@ "client_cert_import": "Importer", "client_cert_import_success_msg": "Klient certifikat er importeret", "client_cert_invalid_msg": "Invalid certifikat fil eller forkert adgangskode", + "client_cert_password_message": "Skriv kodeord til dette certifikat", + "client_cert_password_title": "Kodeord til certifikat", "client_cert_remove_msg": "Klient certifikat er fjernet", "client_cert_subtitle": "Supportere kun PKCS12 (.p12, .pfx) format. Certifikat importering/fjernelse er kun tilgængeligt før login", "client_cert_title": "SSL Klient Certifikat [EKSPERIMENTAL]", @@ -864,8 +869,9 @@ "custom_locale": "Brugerdefineret lokale", "custom_locale_description": "Formatér datoer og tal baseret på sproget og regionen", "custom_url": "Tilpasset URL", - "cutoff_date_description": "Fjern fotos og videoer ældre end", - "cutoff_day": "{antal, flertal, en {day} andre {days}}", + "cutoff_date_description": "Behold fotos fra den sidste…", + "cutoff_day": "{count, plural, one {dag} other {dage}}", + "cutoff_year": "{count, plural, one {år} other {år}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Mørk", @@ -919,7 +925,7 @@ "description_input_hint_text": "Tilføj en beskrivelse...", "description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer", "deselect_all": "Afmarkér alt", - "details": "DETALJER", + "details": "Detaljer", "direction": "Retning", "disable": "Deaktiver", "disabled": "Deaktiveret", @@ -991,6 +997,11 @@ "editor_close_without_save_prompt": "Ændringerne vil ikke blive gemt", "editor_close_without_save_title": "Luk editor?", "editor_confirm_reset_all_changes": "Er du sikker på, at du vil nulstille alle ændringer?", + "editor_discard_edits_confirm": "Kassér redigeringer", + "editor_discard_edits_prompt": "Du har ikke‑gemte redigeringer. Er du sikker på, at du vil kassere dem?", + "editor_discard_edits_title": "Kassér ændringer?", + "editor_edits_applied_error": "Kunne ikke gemme redigeringer", + "editor_edits_applied_success": "Redigeringer gemt", "editor_flip_horizontal": "Vend horisontalt", "editor_flip_vertical": "Flip vertikal", "editor_orientation": "Orientering", @@ -1018,9 +1029,11 @@ "error_loading_albums": "Fejl ved indlæsning af album", "error_loading_image": "Fejl ved indlæsning af billede", "error_loading_partners": "Fejl ved indlæsning af partnere: {error}", + "error_retrieving_asset_information": "Fejl ved hentning af objekt-data", "error_saving_image": "Fejl: {error}", "error_tag_face_bounding_box": "Fejl ved tagging af ansigt - kan ikke finde koordinator for afgrænsningskasse", "error_title": "Fejl - Noget gik galt", + "error_while_navigating": "Fejl ved navigering til objekt", "errors": { "cannot_navigate_next_asset": "Kan ikke navigere til næste mediefil", "cannot_navigate_previous_asset": "Kan ikke navigere til forrige mediefil", @@ -1190,6 +1203,8 @@ "features_in_development": "Funktioner under udvikling", "features_setting_description": "Administrer app-funktioner", "file_name_or_extension": "Filnavn eller filtype", + "file_name_text": "Filnavn", + "file_name_with_value": "Filnavn: {file_name}", "file_size": "Fil størrelse", "filename": "Filnavn", "filetype": "Filtype", @@ -1208,7 +1223,7 @@ "forgot_pin_code_question": "Har du glemt PIN-koden?", "forward": "Fremad", "free_up_space": "Frigør plads", - "free_up_space_description": "Flyt sikkerhedskopierede fotos og videoer til din enheds skraldepapir for at frigøre plads. Dine kopier på serveren forbliver sikre", + "free_up_space_description": "Flyt sikkerhedskopierede fotos og videoer til din enheds skraldespand for at frigøre plads. Dine kopier på serveren forbliver sikre.", "free_up_space_settings_subtitle": "Frigør enhedslagerplads", "full_path": "Fuld sti: {path}", "gcast_enabled": "Google Cast", @@ -1325,9 +1340,15 @@ "json_editor": "JSON editor", "json_error": "JSON fejl", "keep": "Behold", + "keep_albums": "Behold albums", + "keep_albums_count": "Beholder {count} {count, plural, one {album} other {albums}}", "keep_all": "Behold alle", + "keep_description": "Vælg hvad der skal forblive på din enhed efter oprydning af plads.", "keep_favorites": "Behold favoritter", + "keep_on_device": "Behold på enheden", + "keep_on_device_hint": "Vælg elementer, der skal beholdes på denne enhed", "keep_this_delete_others": "Behold dette, slet andre", + "keeping": "Beholder: {items}", "kept_this_deleted_others": "Beholdt denne mediefil og slettede {count, plural, one {# aktiv} other {# aktiver}}", "keyboard_shortcuts": "Tastaturgenveje", "language": "Sprog", @@ -1421,10 +1442,28 @@ "loop_videos_description": "Aktivér for at genafspille videoer automatisk i detaljeret visning.", "main_branch_warning": "Du bruger en udviklingsversion; vi anbefaler kraftigt at bruge en udgivelsesversion!", "main_menu": "Hovedmenu", + "maintenance_action_restore": "Genopretter database", "maintenance_description": "Immich er blevet sat i vedligeholdelsestilstand.", "maintenance_end": "Afslut vedligeholdelsestilstand", "maintenance_end_error": "Vedligeholdelsestilstand kunne ikke afsluttes.", "maintenance_logged_in_as": "Aktuelt logget ind som {user}", + "maintenance_restore_from_backup": "Genskab fra sikkerhedskopi", + "maintenance_restore_library": "Genskab dit bibliotek", + "maintenance_restore_library_confirm": "Hvis dette ser korrekt ud, så fortsæt for at genoprette fra en sikkerhedskopi!", + "maintenance_restore_library_description": "Genopretter database", + "maintenance_restore_library_folder_has_files": "{folder} har {count, plural, one {mappe} other {mapper}}", + "maintenance_restore_library_folder_no_files": "{folder} mangler filer!", + "maintenance_restore_library_folder_pass": "læs- og skrivbar", + "maintenance_restore_library_folder_read_fail": "ikke læsbar", + "maintenance_restore_library_folder_write_fail": "ikke skrivbar", + "maintenance_restore_library_hint_missing_files": "Du mangler måske vigtige filer", + "maintenance_restore_library_hint_regenerate_later": "Du kan genindstille disse senere, i indstillinger", + "maintenance_restore_library_hint_storage_template_missing_files": "Bruger du en lagringsskabelon? Du mangler måske nogle filer", + "maintenance_restore_library_loading": "Indlæser integritetskontroller og heuristikker …", + "maintenance_task_backup": "Laver en backup af den eksisterende database …", + "maintenance_task_migrations": "Kører migration af database…", + "maintenance_task_restore": "Genskaber den valgte backup…", + "maintenance_task_rollback": "Genoprettelse slog fejl, ruller tilbage til genoprettelsespunkt…", "maintenance_title": "Midlertidigt Utilgængelig", "make": "Producent", "manage_geolocation": "Administrer placering", @@ -1544,7 +1583,7 @@ "no_albums_with_name_yet": "Det ser ud til, at du ikke har noget album med dette navn endnu.", "no_albums_yet": "Det ser ud til, at du ikke har nogen album endnu.", "no_archived_assets_message": "Arkivér billeder og videoer for at gemme dem væk fra din billedoversigt", - "no_assets_message": "KLIK FOR AT UPLOADE DIT FØRSTE BILLEDE", + "no_assets_message": "Klik for at uploade dit første foto", "no_assets_to_show": "Ingen elementer at vise", "no_cast_devices_found": "Ingen Cast-enheder fundet", "no_checksum_local": "Ingen checksum tilgængelig – kan ikke hente lokale objekter", @@ -1574,7 +1613,6 @@ "not_available": "ikke tilgængelig", "not_in_any_album": "Ikke i noget album", "not_selected": "Ikke valgt", - "note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede medier, kør opgaven igen", "notes": "Noter", "nothing_here_yet": "Intet her endnu", "notification_permission_dialog_content": "Gå til indstillinger for at slå notifikationer til.", @@ -1686,7 +1724,7 @@ "photos_from_previous_years": "Billeder fra tidligere år", "photos_only": "Kun fotos", "pick_a_location": "Vælg et sted", - "pick_custom_range": "Brugerdefineret periode", + "pick_custom_range": "Brugerdefineret interval", "pick_date_range": "Vælg et datointerval", "pin_code_changed_successfully": "Ændring af PIN kode lykkedes", "pin_code_reset_successfully": "Nulstilling af PIN kode lykkedes", @@ -1765,6 +1803,7 @@ "rating_clear": "Nulstil vurdering", "rating_count": "{count, plural, one {# stjerne} other {# stjerner}}", "rating_description": "Vis EXIF-klassificeringen i infopanelet", + "rating_set": "Vurdering sat til {rating, plural, one {# stjerne} other {# stjerner}}", "reaction_options": "Reaktionsindstillinger", "read_changelog": "Læs ændringslog", "readonly_mode_disabled": "Skrivebeskyttet tilstand deaktiveret", @@ -1775,7 +1814,7 @@ "reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person", "reassing_hint": "Tildel valgte mediefiler til en eksisterende person", "recent": "For nylig", - "recent-albums": "Seneste albums", + "recent_albums": "Seneste albums", "recent_searches": "Seneste søgninger", "recently_added": "Senest tilføjet", "recently_added_page_title": "Nyligt tilføjet", @@ -1864,11 +1903,11 @@ "saved_settings": "Gemte indstillinger", "say_something": "Skriv noget", "scaffold_body_error_occurred": "Der opstod en fejl", - "scan": "Scan", + "scan": "Skan", "scan_all_libraries": "Skan alle biblioteker", "scan_library": "Skan", "scan_settings": "Skanningsindstillinger", - "scanning": "Scanning", + "scanning": "Skanner", "scanning_for_album": "Skanner efter albummer...", "search": "Søg", "search_albums": "Søg i albummer", @@ -2041,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} år", "shared_link_edit_password_hint": "Indtast kodeordet", "shared_link_edit_submit_button": "Opdater link", - "shared_link_error_server_url_fetch": "Kan ikke finde server URL", + "shared_link_error_server_url_fetch": "Kan ikke hente server URL", "shared_link_expires_day": "Udløber om {count} dag", "shared_link_expires_days": "Udløber om {count} dage", "shared_link_expires_hour": "Udløber om {count} time", @@ -2128,7 +2167,7 @@ "start_date_before_end_date": "Startdato skal ligge før slutdato", "state": "Stat", "status": "Status", - "stop_casting": "Stop casting", + "stop_casting": "Stop med at caste", "stop_motion_photo": "Stopmotionbillede", "stop_photo_sharing": "Stop med at dele dine billeder?", "stop_photo_sharing_description": "{partner} vil ikke længere kunne tilgå dine billeder.", @@ -2181,6 +2220,7 @@ "theme_setting_theme_subtitle": "Vælg appens temaindstilling", "theme_setting_three_stage_loading_subtitle": "Tre-trins indlæsning kan øge ydeevnen, men kan ligeledes føre til højere netværksbelastning", "theme_setting_three_stage_loading_title": "Slå tre-trins indlæsning til", + "then": "Siden", "they_will_be_merged_together": "De vil blive slået sammen", "third_party_resources": "Tredjepartsressourcer", "time": "Tid", @@ -2265,6 +2305,7 @@ "upload_details": "Upload detaljer", "upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?", "upload_dialog_title": "Upload element", + "upload_error_with_count": "Upload-fejl for {count, plural, one {# objekt} other {# objekter}}", "upload_errors": "Upload afsluttet med {count, plural, one {# fejl} other {# fejl}}. Opdater siden for at se nye uploadaktiver.", "upload_finished": "Upload fuldført", "upload_progress": "Resterende {remaining, number} - Behandlet {processed, number}/{total, number}", diff --git a/i18n/de.json b/i18n/de.json index 8959e20831..b32ac57aba 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1,8 +1,8 @@ { - "about": "Über", + "about": "Über Immich", "account": "Konto", "account_settings": "Kontoeinstellungen", - "acknowledge": "Bestätigen", + "acknowledge": "Verstanden", "action": "Aktion", "action_common_update": "Aktualisieren", "action_description": "Eine Reihe von Aktionen, die an den gefilterten Assets ausgeführt werden sollen", @@ -573,8 +573,8 @@ "asset_list_settings_subtitle": "Einstellungen für das Fotogitter-Layout", "asset_list_settings_title": "Fotogitter", "asset_not_found_on_device_android": "Datei auf Gerät nicht gefunden", - "asset_not_found_on_device_ios": "Datei auf Gerät nicht gefunden. Wenn Du iCloud verwendest, kann die Datei möglicherweise nicht auffindbar sein aufgrund schlechter Dateispeicherung von iCloud", - "asset_not_found_on_icloud": "Datei in iCloud nicht gefunden. Die Datei kann möglicherweise nicht auffindbar sein aufgrund schlechter Dateispeicherung in iCloud", + "asset_not_found_on_device_ios": "Datei auf Gerät nicht gefunden. Wenn Du iCloud verwendest, kann die Datei möglicherweise aufgrund schlechter Dateispeicherung von iCloud nicht auffindbar sein", + "asset_not_found_on_icloud": "Datei in iCloud nicht gefunden. Die Datei kann möglicherweise aufgrund schlechter Dateispeicherung in iCloud nicht auffindbar sein", "asset_offline": "Datei offline", "asset_offline_description": "Diese externe Datei ist nicht mehr auf dem Datenträger vorhanden. Bitte wende dich an deinen Immich-Administrator, um Hilfe zu erhalten.", "asset_restored_successfully": "Datei erfolgreich wiederhergestellt", @@ -782,6 +782,8 @@ "client_cert_import": "Importieren", "client_cert_import_success_msg": "Client Zertifikat wurde importiert", "client_cert_invalid_msg": "Ungültige Zertifikatsdatei oder falsches Passwort", + "client_cert_password_message": "Passwort für dieses Zertifikat angeben", + "client_cert_password_title": "Passwort des Zertifikats", "client_cert_remove_msg": "Client Zertifikat wurde entfernt", "client_cert_subtitle": "Unterstützt nur das PKCS12 (.p12, .pfx) Format. Zertifikatsimporte oder -entfernungen sind nur vor dem Login möglich", "client_cert_title": "SSL-Client-Zertifikat [Experimentell]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "Die Änderungen werden nicht gespeichert", "editor_close_without_save_title": "Editor schließen?", "editor_confirm_reset_all_changes": "Alle Änderungen zurücksetzen?", + "editor_discard_edits_confirm": "Änderungen verwerfen", + "editor_discard_edits_prompt": "Es liegen ungespeicherte Änderungen vorhanden. Sicher, dass diese verworfen werden sollen?", + "editor_discard_edits_title": "Änderungen verwerfen?", + "editor_edits_applied_error": "Änderungen konnten nicht angewendet werden", + "editor_edits_applied_success": "Änderungen erfolgreich angewendet", "editor_flip_horizontal": "Horizontal spiegeln", "editor_flip_vertical": "Vertikal spiegeln", "editor_orientation": "Ausrichtung", @@ -1196,6 +1203,8 @@ "features_in_development": "Feature in Entwicklung", "features_setting_description": "Funktionen der App verwalten", "file_name_or_extension": "Dateiname oder -erweiterung", + "file_name_text": "Dateiname", + "file_name_with_value": "Dateiname: {file_name}", "file_size": "Dateigröße", "filename": "Dateiname", "filetype": "Dateityp", @@ -1604,7 +1613,6 @@ "not_available": "N/A", "not_in_any_album": "In keinem Album", "not_selected": "Nicht ausgewählt", - "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um eine Speicherpfadbezeichnung anzuwenden, starte den", "notes": "Notizen", "nothing_here_yet": "Noch nichts hier", "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\".", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} einer neuen Person zugewiesen", "reassing_hint": "Markierte Dateien einer vorhandenen Person zuweisen", "recent": "Neueste", - "recent-albums": "Neueste Alben", + "recent_albums": "Neueste Alben", "recent_searches": "Letzte Suchen", "recently_added": "Kürzlich hinzugefügt", "recently_added_page_title": "Zuletzt hinzugefügt", @@ -1904,7 +1912,7 @@ "search": "Suche", "search_albums": "Album suchen", "search_by_context": "Suche nach Kontext", - "search_by_description": "Nach Beschreibung suchen", + "search_by_description": "Suche nach Beschreibung", "search_by_description_example": "Wandern in Sapa", "search_by_filename": "Suche nach Dateiname oder -erweiterung", "search_by_filename_example": "z.B. IMG_1234.JPG oder PNG", diff --git a/i18n/de_CH.json b/i18n/de_CH.json index 52bff4839f..1925b4b8a4 100644 --- a/i18n/de_CH.json +++ b/i18n/de_CH.json @@ -5,12 +5,12 @@ "acknowledge": "Bestätige", "action": "Aktion", "action_common_update": "Update", - "action_description": "Eine Reihe von Aktionen, die an den gefilterten Assets ausgeführt werden sollen", + "action_description": "Es paar Aktione, wo a de gfilterete Assets usgführt wärde sölled", "actions": "Aktione", "active": "Aktiv", "active_count": "Aktivi: {count}", "activity": "Aktivität", - "activity_changed": "Aktivität ist {enabled, select, true {aktiviert} other {deaktiviert}}", + "activity_changed": "Aktivität isch {enabled, select, true {aktiviert} other {deaktiviert}}", "add": "Hinzuefüegä", "add_a_description": "Beschriibig hinzuefüege", "add_a_location": "Standort hinzuefüege", @@ -18,41 +18,42 @@ "add_a_title": "Titel hinzuefüege", "add_action": "Aktion hinzuefüege", "add_action_description": "Aklicke um en Aktion dure zfüehre", + "add_assets": "Assets hinzufüege", "add_birthday": "Geburtstag hinzuefüege", "add_endpoint": "Endpunkt hinzuefüge", - "add_exclusion_pattern": "Exklusions muster hinzuefüege", + "add_exclusion_pattern": "Uuschlussmuster hinzuefüege", "add_filter": "Filter hinzuefüge", - "add_filter_description": "Klicken, um eine Filterbedingung hinzuzufügen", + "add_filter_description": "Klicke, um e Filterbedingig hinzuezfüege", "add_location": "Standort hinzuefüege", "add_more_users": "Meh Benutzer hinzuefüege", - "add_partner": "Partner hinzufügen", + "add_partner": "Partner hinzuefüege", "add_path": "Pfad hinzuefüege", "add_photos": "Föteli hinzuefüege", - "add_tag": "Tag hinzufügen", + "add_tag": "Tag hinzuefüege", "add_to": "Hinzuefüege zu …", "add_to_album": "Zum Album hinzuefüege", - "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_added": "Zu {album} hinzuegfüegt", + "add_to_album_bottom_sheet_already_exists": "Scho in {album}", "add_to_album_bottom_sheet_some_local_assets": "Es hend es paar lokali Dateie nöd chöne im Album hinzuegfüegt werde", - "add_to_album_toggle": "Auswahl umschalten für {album}", + "add_to_album_toggle": "Uuswahl umschalte für {album}", "add_to_albums": "Zu Albe hinzuefüege", - "add_to_albums_count": "Zu Alben hinzufügen ({count})", + "add_to_albums_count": "Zu Albe hinzuefüege ({count})", "add_to_bottom_bar": "Hinzuefüege zu", "add_to_shared_album": "Zum teilte Album hinzuefüege", "add_upload_to_stack": "Upload zum Stack hinzuefüege", "add_url": "URL hinzuefüege", - "add_workflow_step": "Workflow-Schritt hinzufügen", + "add_workflow_step": "Workflow-Schritt hinzuefüege", "added_to_archive": "Is Archiv verschobe", "added_to_favorites": "Zu dine Favoritä hinzuegfüegt", - "added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt", + "added_to_favorites_count": "{count, number} zu Favorite hinzuegfüegt", "admin": { - "add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw\" zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.", + "add_exclusion_pattern_description": "Uusschlussmuster hinzuefüge. Platzhalter, wie *, **, und ? wärded understützt. Zum all Dateie i eim Verzeichnis namens „Raw\" ignoriere, „**/Raw/**“ verwände. Zum all Dateien ignorieren, wo uf „.tif“ änded, „**/*.tif“ verwände. Zum en absolute Pfad ignoriere, „/pfad/zum/ignoriere/**“ verwände.", "admin_user": "Admin Benutzer", - "asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.", + "asset_offline_description": "Die Datei vonere externe Bibliothek isch nümme uf de Festplatte und isch in Papierchorb verschobe worde. Falls die Datei innerhalb vo de Bibliothek verschoben worde isch, überprüf dini Ziitleiste uf die neui entsprechendi Datei. Zum die Datei wiederherstelle, stell bitte sicher, dass Immich uf de unde stehendi Dateipfad chan zuegriife und scann d'Bibliothek.", "authentication_settings": "Authentifizierigs Iistellige", "authentication_settings_description": "Passwort, OAuth und anderi Authentifizierigseinstellige verwalte", "authentication_settings_disable_all": "Bisch sicher, dass du alli Login-Methodä wotsch deaktivierä? S Login isch denn komplett deaktiviert.", - "authentication_settings_reenable": "Nutze einen Server-Befehl zur Reaktivierung.", + "authentication_settings_reenable": "Bruuch ein Server-Befehl zum reaktiviere.", "background_task_job": "Hintergrund Ufgabä", "backup_database": "Datenbank-Dump aalege", "backup_database_enable_description": "Datenbank-Dumps aktiviere", @@ -72,15 +73,15 @@ "confirm_delete_library_assets": "Bisch sicher, dass du die Bibliothek wotsch lösche? Das löscht {count, plural, one {# enthaltenes Asset} other {alli # enthaltene Assets}} us Immich und chan nöd rückgängig gmacht werde. D Dateie bliibed uf em Dateträger.", "confirm_email_below": "Zum bestätige bitte \"{email}\" une iitippe", "confirm_reprocess_all_faces": "Bisch sicher, dass du alli Gsichter neu verarbeite wotsch? Däbii werde au benannti Persone glöscht.", - "confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?", - "confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN-Code von {user} zurücksetzen möchtest?", + "confirm_user_password_reset": "Bisch sicher, dass du s Passwort für {user} möchtisch zruggsetze?", + "confirm_user_pin_code_reset": "Bisch sicher, dass du de PIN-Code vo {user} möchtisch zruggsetze?", "copy_config_to_clipboard_description": "Kopiere die aktuelle Systemkonfiguration als JSON-Objekt in die Zwischenablage", - "create_job": "Aufgabe erstellen", - "cron_expression": "Cron-Zeitangabe", - "cron_expression_description": "Setze das Scanintervall im Cron-Format. Hilfe mit dem Format bietet dir dabei z. B. der Crontab Guru", - "cron_expression_presets": "Vorlagen für Cron-Ausdruck", + "create_job": "Uufgabe erstelle", + "cron_expression": "Cron-Ziitagabe", + "cron_expression_description": "Setz s Scanintervall im Cron-Format. Hilf mit däm Format bütet z. B. der Crontab Guru", + "cron_expression_presets": "Vorlage für Cron-Uusdruck", "disable_login": "Login deaktiviere", - "duplicate_detection_job_description": "Diese Aufgabe führt das maschinelle Lernen für jede Datei aus, um Duplikate zu finden. Diese Aufgabe beruht auf der intelligenten Suche", + "duplicate_detection_job_description": "Die Uufgab füehrt s maschinelle Lärne für jedi Datei us, zum Duplikat finde. Die Uufgabe berueht uf de intelligente Suechi", "exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen Ihrer Bibliothek ignoriert werden. Dies ist nützlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren möchtest, wie z. B. RAW-Dateien.", "export_config_as_json_description": "Lade die aktuelle Systemkonfiguration als JSON-Datei herunter", "external_libraries_page_description": "Externe Bibliotheksseite für Administratoren", diff --git a/i18n/el.json b/i18n/el.json index 072e283a72..b1a868023e 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -104,6 +104,8 @@ "image_preview_description": "Μεσαίου μεγέθους εικόνες, χωρίς μεταδεδομένα, οι οποίες χρησιμοποιούνται στην προβολή ενός αντικειμένου και για μηχανική μάθηση", "image_preview_quality_description": "Ποιότητα προεπισκόπησης από 1 έως 100. Όσο μεγαλύτερη τιμή τόσο καλύτερη η ποιότητα, αλλά παράγονται μεγαλύτερα αρχεία που ενδέχεται να μειώσουν την ταχύτητα απόκρισης της εφαρμογής. Οι χαμηλές τιμές μπορεί να επηρεάσουν τη ποιότητα της μηχανικής μάθησης.", "image_preview_title": "Ρυθμίσεις Προεπισκόπισης", + "image_progressive": "Προοδευτικό", + "image_progressive_description": "Προοδευτική κωδικοποίηση εικόνων JPEG για σταδιακή φόρτωση κατά την προβολή. Δεν επηρεάζει τις εικόνες WebP.", "image_quality": "Ποιότητα", "image_resolution": "Ανάλυση", "image_resolution_description": "Υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο να κωδικοποιηθούν, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.", @@ -270,7 +272,7 @@ "oauth_auto_register": "Αυτόματη καταχώρηση", "oauth_auto_register_description": "Αυτόματη καταχώρηση νέου χρήστη αφού συνδεθεί με OAuth", "oauth_button_text": "Κείμενο κουμπιού", - "oauth_client_secret_description": "Υποχρεωτικό εαν PKCE (Proof Key for Code Exchange) δεν υποστηρίζεται από τον OAuth πάροχο", + "oauth_client_secret_description": "Απαιτείται για έμπιστο πρόγραμμα πελάτη ή αν δεν υποστηρίζεται PKCE (Proof Key for Code Exchange) σε δημόσιο πρόγραμμα πελάτη.", "oauth_enable_description": "Σύνδεση με OAuth", "oauth_mobile_redirect_uri": "URI Ανακατεύθυνσης για κινητά τηλέφωνα", "oauth_mobile_redirect_uri_override": "Προσπέλαση URI ανακατεύθυνσης για κινητά τηλέφωνα", @@ -349,7 +351,7 @@ "template_settings": "Πρότυπα ειδοποιήσεων", "template_settings_description": "Διαχείριση προσαρμοσμένων προτύπων για ειδοποιήσεις", "theme_custom_css_settings": "Προσαρμοσμένο CSS", - "theme_custom_css_settings_description": "Τα Cascading Style Sheets(CSS) επιτρέπει την προσαρμογή του σχεδιασμού του Immich.", + "theme_custom_css_settings_description": "Τα Cascading Style Sheets επιτρέπει την προσαρμογή του σχεδιασμού του Immich.", "theme_settings": "Ρυθμίσεις θέματος", "theme_settings_description": "Διαχείριση της προσαρμογής του ιστότοπου του Immich", "thumbnail_generation_job": "Δημιουργία Μικρογραφιών", @@ -449,6 +451,9 @@ "admin_password": "Κωδικός πρόσβασης Διαχειριστή", "administration": "Διαχείριση", "advanced": "Για προχωρημένους", + "advanced_settings_clear_image_cache": "Καθαρισμός προσωρινής μνήμης εικόνων", + "advanced_settings_clear_image_cache_error": "Αποτυχία καθαρισμού προσωρινής μνήμης εικόνων", + "advanced_settings_clear_image_cache_success": "Επιτυχής εκκαθάριση {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Χρησιμοποιήστε αυτήν την επιλογή για να φιλτράρετε τα μέσα ενημέρωσης κατά τον συγχρονισμό με βάση εναλλακτικά κριτήρια. Δοκιμάστε αυτή τη δυνατότητα μόνο αν έχετε προβλήματα με την εφαρμογή που εντοπίζει όλα τα άλμπουμ.", "advanced_settings_enable_alternate_media_filter_title": "[ΠΕΙΡΑΜΑΤΙΚΟ] Χρήση εναλλακτικού φίλτρου συγχρονισμού άλμπουμ συσκευής", "advanced_settings_log_level_title": "Επίπεδο σύνδεσης: {level}", @@ -512,6 +517,7 @@ "all": "Όλα", "all_albums": "Όλα τα άλμπουμ", "all_people": "Όλα τα άτομα", + "all_photos": "Όλες οι φωτογραφίες", "all_videos": "Όλα τα βίντεο", "allow_dark_mode": "Επιτρέψτε τη σκοτεινή λειτουργία", "allow_edits": "Επιτρέψτε τις τροποποιήσεις", @@ -519,6 +525,9 @@ "allow_public_user_to_upload": "Επιτρέψτε στον δημόσιο χρήστη να ανεβάσει", "allowed": "Επιτρεπόμενο", "alt_text_qr_code": "Εικόνα κωδικού QR", + "always_keep": "Διατήρηση πάντα", + "always_keep_photos_hint": "Η «Απελευθέρωση χώρου» θα κρατήσει όλες τις φωτογραφίες σε αυτήν τη συσκευή.", + "always_keep_videos_hint": "Η «Απελευθέρωση χώρου» θα κρατήσει όλα τα βίντεο σε αυτήν τη συσκευή.", "anti_clockwise": "Αντίθετα με τη φορά του ρολογιού", "api_key": "Κλειδί API", "api_key_description": "Αυτή η τιμή θα εμφανιστεί μόνο μία φορά. Παρακαλώ βεβαιωθείτε ότι την έχετε αντιγράψει πριν κλείσετε το παράθυρο.", @@ -563,6 +572,9 @@ "asset_list_layout_sub_title": "Διάταξη", "asset_list_settings_subtitle": "Ρυθμίσεις διάταξης πλέγματος φωτογραφιών", "asset_list_settings_title": "Πλέγμα φωτογραφιών", + "asset_not_found_on_device_android": "Το στοιχείο δεν βρέθηκε στη συσκευή", + "asset_not_found_on_device_ios": "Το στοιχείο δεν βρέθηκε στη συσκευή. Αν χρησιμοποιείτε iCloud, μπορεί να μην είναι προσβάσιμο λόγω προβληματικού αρχείου που είναι αποθηκευμένο στο iCloud", + "asset_not_found_on_icloud": "Το στοιχείο δεν βρέθηκε στο iCloud. Μπορεί να μην είναι προσβάσιμο λόγω προβληματικού αρχείου που είναι αποθηκευμένο στο iCloud", "asset_offline": "Αντικείμενο εκτός σύνδεσης", "asset_offline_description": "Αυτό το εξωτερικό αντικείμενο δεν βρέθηκε πλέον στον δίσκο. Παρακαλώ επικοινωνήστε με τον διαχειριστή του Immich για βοήθεια.", "asset_restored_successfully": "Το στοιχείο αποκαταστάθηκε με επιτυχία", @@ -752,11 +764,12 @@ "cleanup_deleted_assets": "Μεταφέρθηκαν {count} αρχεία στον κάδο της συσκευής", "cleanup_deleting": "Μεταφορά στον κάδο…", "cleanup_found_assets": "Βρέθηκαν {count} αρχεία που έχουν αντιγραφεί ασφαλώς", + "cleanup_found_assets_with_size": "Βρέθηκαν {count} στοιχεία αντιγράφου ασφαλείας ({size})", "cleanup_icloud_shared_albums_excluded": "Τα Κοινόχρηστα Άλμπουμ iCloud εξαιρούνται από τη σάρωση", - "cleanup_no_assets_found": "Δεν βρέθηκαν αντίγραφα ασφαλείας στοιχείων που να ταιριάζουν με τα κριτήρια σου", + "cleanup_no_assets_found": "Δεν βρέθηκαν στοιχεία που να ταιριάζουν με τα παραπάνω κριτήρια. Η «Απελευθέρωση χώρου» μπορεί να διαγράψει μόνο τα στοιχεία που έχουν αντιγραφεί ασφαλώς στο διακομιστή", "cleanup_preview_title": "Στοιχεία προς διαγραφή ({count})", - "cleanup_step3_description": "Σάρωση για φωτογραφίες και βίντεο που έχουν αντιγραφεί στον διακομιστή με την επιλεγμένη ημερομηνία και τα επιλεγμένα φίλτρα", - "cleanup_step4_summary": "{count} αρχεία που δημιουργήθηκαν πριν από {date} έχουν τοποθετηθεί σε σειρά για διαγραφή από τη συσκευή σας", + "cleanup_step3_description": "Σάρωση για στοιχεία που έχουν αντιγραφεί ασφαλώς σύμφωνα με την ημερομηνία και τις ρυθμίσεις διατήρησης.", + "cleanup_step4_summary": "{count} στοιχεία (δημιουργήθηκαν πριν από {date}) προς διαγραφή από τη συσκευή σας. Οι φωτογραφίες θα παραμείνουν προσβάσιμες από την εφαρμογή Immich.", "cleanup_trash_hint": "Για την πλήρη απελευθέρωση του χώρου αποθήκευσης, ανοίξτε την εφαρμογή φωτογραφιών του συστήματός σας και αδειάστε τον κάδο", "clear": "Εκκαθάριση", "clear_all": "Εκκαθάριση όλων", @@ -769,6 +782,8 @@ "client_cert_import": "Εισαγωγή", "client_cert_import_success_msg": "Το πιστοποιητικό πελάτη εισάγεται", "client_cert_invalid_msg": "Μη έγκυρο αρχείο πιστοποιητικού ή λάθος κωδικός πρόσβασης", + "client_cert_password_message": "Εισάγετε τον κωδικό πρόσβασης για αυτό το πιστοποιητικό", + "client_cert_password_title": "Κωδικός πιστοποιητικού", "client_cert_remove_msg": "Το πιστοποιητικό πελάτη καταργήθηκε", "client_cert_subtitle": "Υποστηρίζει μόνο τη μορφή PKCS12 (.p12, .pfx). Η εισαγωγή/αφαίρεση πιστοποιητικού είναι διαθέσιμη μόνο πριν από τη σύνδεση", "client_cert_title": "Πιστοποιητικό SSL πελάτη [ΠΕΙΡΑΜΑΤΙΚΟ]", @@ -854,7 +869,7 @@ "custom_locale": "Προσαρμοσμένη Τοπική Ρύθμιση", "custom_locale_description": "Μορφοποιήστε τις ημερομηνίες και τους αριθμούς, σύμφωνα με τη γλώσσα και την περιοχή", "custom_url": "Προσαρμοσμένη διεύθυνση URL", - "cutoff_date_description": "Διαγραφή φωτογραφιών και βίντεο παλαιότερων από", + "cutoff_date_description": "Διατήρηση φωτογραφιών από τις τελευταίες…", "cutoff_day": "{count, plural, one {ημέρα} other {ημέρες}}", "cutoff_year": "{count, plural, one {έτος} other {έτη}}", "daily_title_text_date": "Ε, MMM dd", @@ -982,6 +997,11 @@ "editor_close_without_save_prompt": "Αυτές οι αλλαγές δεν θα αποθηκευτούν", "editor_close_without_save_title": "Κλείσιμο επεξεργαστή;", "editor_confirm_reset_all_changes": "Είστε σίγουροι ότι θέλετε να επαναφέρετε όλες τις αλλαγές;", + "editor_discard_edits_confirm": "Απόρριψη αλλαγών", + "editor_discard_edits_prompt": "Έχετε μη αποθηκευμένες αλλαγές. Είστε σίγουροι ότι θέλετε να τις απορρίψετε;", + "editor_discard_edits_title": "Απόρριψη αλλαγών;", + "editor_edits_applied_error": "Αποτυχία εφαρμογής αλλαγών", + "editor_edits_applied_success": "Οι αλλαγές εφαρμόστηκαν με επιτυχία", "editor_flip_horizontal": "Οριζόντια αναστροφή", "editor_flip_vertical": "Κάθετη αναστροφή", "editor_orientation": "Προσανατολισμός", @@ -1006,6 +1026,7 @@ "error_change_sort_album": "Απέτυχε η αλλαγή σειράς του άλμπουμ", "error_delete_face": "Σφάλμα διαγραφής προσώπου από το στοιχείο", "error_getting_places": "Σφάλμα κατά την ανάκτηση τοποθεσιών", + "error_loading_albums": "Σφάλμα κατά τη φόρτωση των άλμπουμ", "error_loading_image": "Σφάλμα κατά τη φόρτωση της εικόνας", "error_loading_partners": "Σφάλμα κατά τη φόρτωση συνεργατών: {error}", "error_retrieving_asset_information": "Σφάλμα κατά την ανάκτηση πληροφοριών στοιχείου", @@ -1182,6 +1203,8 @@ "features_in_development": "Λειτουργίες υπό Ανάπτυξη", "features_setting_description": "Διαχειριστείτε τα χαρακτηριστικά της εφαρμογής", "file_name_or_extension": "Όνομα αρχείου ή επέκταση", + "file_name_text": "Όνομα αρχείου", + "file_name_with_value": "Όνομα αρχείου: {file_name}", "file_size": "Μέγεθος αρχείου", "filename": "Ονομασία αρχείου", "filetype": "Τύπος αρχείου", @@ -1200,7 +1223,7 @@ "forgot_pin_code_question": "Ξεχάσατε το PIN;", "forward": "Προς τα εμπρός", "free_up_space": "Απελευθέρωση χώρου", - "free_up_space_description": "Μετακινήστε τις φωτογραφίες και τα βίντεο που έχουν αντιγραφεί στον κάδο της συσκευής σας για να απελευθερώσετε χώρο. Τα αντίγραφά σας στον διακομιστή παραμένουν ασφαλή", + "free_up_space_description": "Μετακινήστε τις φωτογραφίες και τα βίντεο που έχουν αντιγραφεί στον κάδο της συσκευής σας για να απελευθερώσετε χώρο. Τα αντίγραφά σας στο διακομιστή, παραμένουν ασφαλή.", "free_up_space_settings_subtitle": "Απελευθέρωση χώρου στη συσκευή", "full_path": "Πλήρης διαδρομή: {path}", "gcast_enabled": "Μετάδοση περιεχομένου Google Cast", @@ -1317,9 +1340,15 @@ "json_editor": "Επεξεργαστής JSON", "json_error": "Σφάλμα JSON", "keep": "Διατήρηση", + "keep_albums": "Διατήρηση των άλμπουμ", + "keep_albums_count": "Διατηρούνται {count} {count, plural, one {άλμπουμ} other {άλμπουμ}}", "keep_all": "Διατήρηση Όλων", + "keep_description": "Επιλέξτε τι θα παραμείνει στη συσκευή σας κατά την απελευθέρωση χώρου.", "keep_favorites": "Διατήρηση αγαπημένων", + "keep_on_device": "Διατήρηση στη συσκευή", + "keep_on_device_hint": "Επιλέξτε τα αντικείμενα που θα κρατήσετε σε αυτήν τη συσκευή", "keep_this_delete_others": "Διατήρηση αυτού, διαγραφή υπολοίπων", + "keeping": "Διατηρούνται: {items}", "kept_this_deleted_others": "Διατηρήθηκε αυτό το στοιχείο και διαγράφηκε/καν {count, plural, one {# στοιχείο} other {# στοιχεία}}", "keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου", "language": "Γλώσσα", @@ -1549,6 +1578,7 @@ "next_memory": "Επόμενη ανάμνηση", "no": "Όχι", "no_actions_added": "Δεν έχουν προστεθεί ακόμα ενέργειες", + "no_albums_found": "Δεν βρέθηκαν άλμπουμ", "no_albums_message": "Δημιουργήστε ένα άλμπουμ για να οργανώσετε τις φωτογραφίες και τα βίντεό σας", "no_albums_with_name_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ με αυτό το όνομα ακόμα.", "no_albums_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ ακόμα.", @@ -1578,11 +1608,11 @@ "no_results_description": "Δοκιμάστε ένα συνώνυμο ή πιο γενική λέξη-κλειδί", "no_shared_albums_message": "Δημιουργήστε ένα άλμπουμ για να μοιράζεστε φωτογραφίες και βίντεο με άτομα στο δίκτυό σας", "no_uploads_in_progress": "Καμία μεταφόρτωση σε εξέλιξη", + "none": "Κανένα", "not_allowed": "Δεν επιτρέπεται", "not_available": "Μ/Δ (Μη Διαθέσιμο)", "not_in_any_album": "Σε κανένα άλμπουμ", "not_selected": "Δεν επιλέχθηκε", - "note_apply_storage_label_to_previously_uploaded assets": "Σημείωση: Για να εφαρμόσετε την Ετικέτα Αποθήκευσης σε στοιχεία που έχουν μεταφορτωθεί προηγουμένως, εκτελέστε το", "notes": "Σημειώσεις", "nothing_here_yet": "Τίποτα εδώ ακόμα", "notification_permission_dialog_content": "Για να ενεργοποιήσετε τις ειδοποιήσεις, μεταβείτε στις Ρυθμίσεις και επιλέξτε να επιτρέπεται.", @@ -1784,7 +1814,7 @@ "reassigned_assets_to_new_person": "Η ανάθεση {count, plural, one {# αρχείου} other {# αρχείων}} σε νέο άτομο", "reassing_hint": "Ανάθεση των επιλεγμένων στοιχείων σε υπάρχον άτομο", "recent": "Πρόσφατα", - "recent-albums": "Πρόσφατα άλμπουμ", + "recent_albums": "Πρόσφατα άλμπουμ", "recent_searches": "Πρόσφατες αναζητήσεις", "recently_added": "Προστέθηκαν πρόσφατα", "recently_added_page_title": "Προστέθηκαν Πρόσφατα", @@ -1907,6 +1937,7 @@ "search_filter_media_type_title": "Επιλέξτε τύπο μέσου", "search_filter_ocr": "Αναζήτηση κατά OCR", "search_filter_people_title": "Επιλέξτε άτομα", + "search_filter_star_rating": "Βαθμολογία με αστέρια", "search_for": "Αναζήτηση για", "search_for_existing_person": "Αναζήτηση υπάρχοντος ατόμου", "search_no_more_result": "Δεν υπάρχουν άλλα αποτελέσματα", @@ -2049,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} έτος", "shared_link_edit_password_hint": "Εισαγάγετε τον κωδικό πρόσβασης κοινής χρήσης", "shared_link_edit_submit_button": "Ενημέρωση συνδέσμου", - "shared_link_error_server_url_fetch": "Δεν είναι δυνατή η ανάκτηση του URL του διακομιστή", + "shared_link_error_server_url_fetch": "Δεν είναι δυνατή η ανάκτηση του url του διακομιστή", "shared_link_expires_day": "Λήγει σε {count} ημέρα", "shared_link_expires_days": "Λήγει σε {count} ημέρες", "shared_link_expires_hour": "Λήγει σε {count} ώρα", @@ -2111,6 +2142,8 @@ "skip_to_folders": "Παράκαμψη στους φακέλους", "skip_to_tags": "Παράκαμψη στις ετικέτες", "slideshow": "Παρουσίαση", + "slideshow_repeat": "Επανάληψη παρουσίασης", + "slideshow_repeat_description": "Εκκίνηση από την αρχή όταν τελειώσει η παρουσίαση", "slideshow_settings": "Ρυθμίσεις παρουσίασης", "sort_albums_by": "Ταξινόμηση άλμπουμ κατά...", "sort_created": "Ημερομηνία Δημιουργίας", @@ -2272,6 +2305,7 @@ "upload_details": "Λεπτομέρειες μεταφόρτωσης", "upload_dialog_info": "Θέλετε να αντιγράψετε (κάνετε backup) τα επιλεγμένo(α) στοιχείο(α) στο διακομιστή;", "upload_dialog_title": "Ανέβασμα στοιχείου", + "upload_error_with_count": "Σφάλμα μεταφόρτωσης για {count, plural, one {# αρχείο} other {# αρχεία}}", "upload_errors": "Η μεταφόρτωση ολοκληρώθηκε με {count, plural, one {# σφάλμα} other {# σφάλματα}}, ανανεώστε τη σελίδα για να δείτε νέα στοιχεία μεταφόρτωσης.", "upload_finished": "Ολοκλήρωση μεταφόρτωσης", "upload_progress": "Απομένουν {remaining, number} - Ολοκληρώθηκαν {processed, number}/{total, number}", diff --git a/i18n/en.json b/i18n/en.json index dedbea1bfe..6e35085be8 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -311,7 +311,7 @@ "search_jobs": "Search jobs…", "send_welcome_email": "Send welcome email", "server_external_domain_settings": "External domain", - "server_external_domain_settings_description": "Domain for public shared links, including http(s)://", + "server_external_domain_settings_description": "Domain used for external links", "server_public_users": "Public Users", "server_public_users_description": "All users (name and email) are listed when adding a user to shared albums. When disabled, the user list will only be available to admin users.", "server_settings": "Server Settings", @@ -794,6 +794,11 @@ "color": "Color", "color_theme": "Color theme", "command": "Command", + "command_palette_prompt": "Quickly find pages, actions, or commands", + "command_palette_to_close": "to close", + "command_palette_to_navigate": "to enter", + "command_palette_to_select": "to select", + "command_palette_to_show_all": "to show all", "comment_deleted": "Comment deleted", "comment_options": "Comment options", "comments_and_likes": "Comments & likes", @@ -1168,6 +1173,7 @@ "exif_bottom_sheet_people": "PEOPLE", "exif_bottom_sheet_person_add_person": "Add name", "exit_slideshow": "Exit Slideshow", + "expand": "Expand", "expand_all": "Expand all", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", @@ -1613,7 +1619,6 @@ "not_available": "N/A", "not_in_any_album": "Not in any album", "not_selected": "Not selected", - "note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the", "notes": "Notes", "nothing_here_yet": "Nothing here yet", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", @@ -1643,6 +1648,7 @@ "online": "Online", "only_favorites": "Only favorites", "open": "Open", + "open_calendar": "Open calendar", "open_in_map_view": "Open in map view", "open_in_openstreetmap": "Open in OpenStreetMap", "open_the_search_filters": "Open the search filters", @@ -1815,7 +1821,7 @@ "reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person", "reassing_hint": "Assign selected assets to an existing person", "recent": "Recent", - "recent-albums": "Recent albums", + "recent_albums": "Recent albums", "recent_searches": "Recent searches", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", @@ -2184,6 +2190,7 @@ "support": "Support", "support_and_feedback": "Support & Feedback", "support_third_party_description": "Your Immich installation was packaged by a third-party. Issues you experience may be caused by that package, so please raise issues with them in the first instance using the links below.", + "supporter": "Supporter", "swap_merge_direction": "Swap merge direction", "sync": "Sync", "sync_albums": "Sync albums", diff --git a/i18n/eo.json b/i18n/eo.json index b77577ea15..6b1ebaacdf 100644 --- a/i18n/eo.json +++ b/i18n/eo.json @@ -394,15 +394,52 @@ "transcoding_policy": "Politiko de transkodado", "transcoding_policy_description": "Kriterioj por indiki ĉu video estas transkodita aŭ ne", "transcoding_preferred_hardware_device": "Preferita aparato", + "transcoding_preferred_hardware_device_description": "Aplikiĝas nur al VAAPI kaj QSV. Indikas la DRI-nodoj uzataj por transkodado per aparato.", + "transcoding_preset_preset": "Antaŭelekto (-preset)", + "transcoding_preset_preset_description": "Rapideco de densigo. Malplia rapideco rezultas je pli malgrandaj dosieroj, kaj plibonigas kvaliton por donita bitrapido. VP9 ignoras rapidecojn pli grandajn ol 'faster'.", + "transcoding_reference_frames": "Referencaj kadroj", + "transcoding_reference_frames_description": "La nombro da apudaj kadroj uzataj dum densigo de iu kadro. Pli granda valoro rezultas je pli bona densigo, sed malpli rapida laboro. Valoro de 0 indikas aŭtomatan agordon.", + "transcoding_required_description": "Nur videoj kun neakceptataj formatoj", + "transcoding_settings": "Agordoj de transkodado de videoj", "transcoding_settings_description": "Administri transkodadon de videoj", + "transcoding_target_resolution": "Celita distingivo", + "transcoding_target_resolution_description": "Pli alta distingivo konservas pli da detaloj, sed bezonas pli da tempo por kodigi, donas pli grandajn dosierojn, kaj povas kaŭzi malrapidecon ĉe la apo.", + "transcoding_temporal_aq": "Adaptema kvantigo de tempo (AQ)", + "transcoding_temporal_aq_description": "Aplikiĝas nur al NVENC. Adaptema kvantigo de tempo (AQ) plibonigas kvaliton de scenoj kun multe da detaloj kaj malmulte da movado. Eble ne funkcios kun malnovaj aparatoj.", + "transcoding_threads": "Fadenoj", + "transcoding_threads_description": "Pli alta valoro ebligas pli rapidan kodadon, sed dume lasas malpli da servila kapacito por aliaj taskoj. La numero ne estu pli ol la nombro da disponeblaj CPU-kernoj. Valoro de 0 indikas maksimuma uzo de disponeblaj rimedoj.", + "transcoding_tone_mapping": "Mapado de tonoj", + "transcoding_tone_mapping_description": "Klopodas konservi aspekton de HDR-videoj dum transkodigo al SDR. Ĉiu algoritmo faras proprajn kompromisojn pri koloroj, detaloj kaj heleco. Hable konservas detalojn, Mobius konservas kolorojn, kaj Reinhard konservas helecon.", + "transcoding_transcode_policy": "Politiko de transkodado", + "transcoding_transcode_policy_description": "Politiko pri kiam video estos transkodita. HDR-videoj ĉiam estas transkoditaj (krom se transkodado estas malŝaltita).", + "transcoding_two_pass_encoding": "Dupasa kodigo", + "transcoding_two_pass_encoding_setting_description": "Transkodigo per du pasoj por krei pli bone kodigitajn videojn. Kiam eblas uzi maksimuman bitrapidon (bezonate por funkcii kun H.264 kaj kun HEVC), tiu ĉi modo uzas gamon de bitrapidoj surbaze de tiu maksimumo, kaj ignoras CRF. Por VP9, eblas uzi CRF se maksimuma bitrapido estas malŝaltita.", + "transcoding_video_codec": "Videa kodeko", + "transcoding_video_codec_description": "VP9 havas altan rendimenton kaj taŭgas por retumiloj, sed bezonas pli da tempo por kodigi. HEVC donas similajn rezultojn, sed malpli da retumiloj rekonas ĝin. H.264 estas vaste rekonata kaj rapide transkodebla, sed la dosieroj estas multe pli grandaj. AV1 estas la plej efika kodeko sed ne bone funkcias kun pli malnovaj aparatoj.", + "trash_enabled_description": "Ŝalti la rubujon", + "trash_number_of_days": "Nombro da tagoj", + "trash_number_of_days_description": "Kiom da tagoj oni konservu elementojn en la rubujo antaŭ ol forigi ilin por ĉiam", + "trash_settings": "Agordoj pri rubujo", "trash_settings_description": "Administri agordojn pri rubaĵoj", + "unlink_all_oauth_accounts": "Malligi ĉiujn OAuth-kontojn", + "unlink_all_oauth_accounts_description": "Ne forgesu malligi ĉiujn OAuth-kontojn antaŭ ol migri al nova provizanto.", + "unlink_all_oauth_accounts_prompt": "Ĉu vi certas, ke vi volas malligi ĉiujn OAuth-kontojn? Tio kreos novan OAuth-identigilon por ĉiu uzanto, kaj ne eblos malfari tion.", + "user_cleanup_job": "Purigado de uzantoj", + "user_delete_delay": "La konto de {user} kaj ĝiaj elementoj estos por ĉiam forigitaj post {delay, plural, one {# tago} other {# tagoj}}.", + "user_delete_delay_settings": "Prokrasto de forigo", + "user_delete_delay_settings_description": "Agordas la nombron da tagoj konserviĝos forigita konto de uzanto, antaŭ ol porĉiama forigo. La porĉiama forigo okazas aŭtomate je noktomezo. Ŝanĝoj al tiu ĉi numero ekhavos efikon je venonta noktomezo.", + "user_delete_immediately": "La konto de {user} estos tuj forigita sed eblos dum kelkaj tagoj retrovi ĝin laŭbezone.", + "user_delete_immediately_checkbox": "Envicigi uzanton kaj ties elementojn por tuja forigo", + "user_details": "Detaloj pri uzanto", + "user_management": "Administrado de uzantoj", + "user_password_has_been_reset": "Pasvorto de tiu ĉi uzanto estas restarigita:", "user_settings_description": "Administri agordojn pri uzantoj" }, "asset_viewer_settings_subtitle": "Administri agordojn pri vidilo de galerioj", "backup_setting_subtitle": "Administri agordojn pri fona kaj malfona alŝutado", "backup_settings_subtitle": "Administri agordojn pri alŝutado", "cleanup_icloud_shared_albums_excluded": "Dividitaj albumoj ĉe iCloud estas ekskluditaj de la analizado", - "cleanup_step3_description": "Serĉi fotojn kaj videojn kun sekurkopio ĉe la servilo, laŭ la elektita limdato kaj filtriloj", + "cleanup_step3_description": "Serĉi fotojn kaj videojn kun sekurkopio ĉe la servilo, laŭ la elektita limdato kaj filtriloj.", "download_settings_description": "Administri agordojn pri elŝutado de elementoj", "edit_exclusion_pattern": "Redakti skemon de ekskludo", "errors": { diff --git a/i18n/es.json b/i18n/es.json index 3f9163481d..49e58e3beb 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -70,7 +70,7 @@ "cleared_jobs": "Trabajos borrados para: {job}", "config_set_by_file": "La configuración está definida por un archivo de configuración", "confirm_delete_library": "¿Estás seguro de que quieres eliminar la biblioteca {library}?", - "confirm_delete_library_assets": "¿Estás seguro de que quieras eliminar esta biblioteca? Esto eliminará los {count, plural, one {# contained asset} other {all # contained assets}} elementos en Immich y no puede deshacerse. Los archivos permanecerán en disco.", + "confirm_delete_library_assets": "¿Estás seguro de que quieras eliminar esta biblioteca? Esto eliminará {count, plural, one {# recurso contenido} other {todos # recursos contenidos}} de Immich y no puede deshacerse. Los archivos permanecerán en disco.", "confirm_email_below": "Para confirmar, escribe \"{email}\" a continuación", "confirm_reprocess_all_faces": "¿Estás seguro de que deseas reprocesar todas las caras? Esto borrará a todas las personas que nombraste.", "confirm_user_password_reset": "¿Estás seguro de que quieres restablecer la contraseña de {user}?", @@ -81,15 +81,15 @@ "cron_expression_description": "Establece el intervalo de escaneo utilizando el formato cron. Para más información puedes consultar, por ejemplo, Crontab Guru", "cron_expression_presets": "Valores predefinidos de expresiones cron", "disable_login": "Deshabilitar inicio de sesión", - "duplicate_detection_job_description": "Lanza el aprendizaje automático para detectar imágenes similares. Necesita tener activado \"Búsqueda Inteligente\"", + "duplicate_detection_job_description": "Ejecuta el aprendizaje automático en los recursos para detectar imágenes similares. Se basa en la búsqueda inteligente", "exclusion_pattern_description": "Los patrones de exclusión te permiten ignorar archivos y carpetas al escanear tu biblioteca. Es útil si tienes carpetas que contienen archivos que no deseas importar, por ejemplo archivos RAW.", "export_config_as_json_description": "Descargar la configuración actual del sistema como un archivo JSON", "external_libraries_page_description": "Página de biblioteca externa del administrador", "face_detection": "Detección de caras", - "face_detection_description": "Detecta las caras en los elementos mediante aprendizaje automático. En el caso de los vídeos, solo se tiene en cuenta la miniatura. \"Actualizar\" (re)procesará todos los elementos. \"Restablecer\" borra además todos los datos de caras actuales. \"Faltante\" pone en cola los elementos que aún no se han procesado. Las caras detectadas se pondrán en cola para el reconocimiento facial una vez finalizada la detección, agrupándolos en personas existentes o nuevas.", + "face_detection_description": "Detecta las caras en los recursos mediante aprendizaje automático. En el caso de los vídeos, solo se tiene en cuenta la miniatura. \"Actualizar\" (re)procesará todos los recursos. \"Restablecer\" borra además todos los datos de caras actuales. \"Faltante\" pone en cola los recursos que aún no se han procesado. Las caras detectadas se pondrán en cola para el reconocimiento facial una vez finalizada la detección, agrupándolos en personas existentes o nuevas.", "facial_recognition_job_description": "Agrupa las caras detectadas en personas. Este paso se realiza después de completar la detección de caras. \"Restablecer\" (re)agrupa todas las caras. \"Faltante\" pone en cola las caras que no tienen una persona asignada.", "failed_job_command": "El comando {command} ha fallado para la tarea: {job}", - "force_delete_user_warning": "CUIDADO: Esta acción eliminará inmediatamente el usuario y todos los elementos. Esta accion no se puede deshacer y los archivos no pueden ser recuperados.", + "force_delete_user_warning": "CUIDADO: Esta acción eliminará inmediatamente el usuario y todos los recursos. Esta acción no se puede deshacer y los archivos no pueden ser recuperados.", "image_format": "Formato", "image_format_description": "WebP genera archivos más pequeños que JPEG, pero es más lento al codificarlos.", "image_fullsize_description": "Imagen de tamaño completo con metadatos removidos, usado cuando se hace zoom", @@ -101,7 +101,7 @@ "image_prefer_embedded_preview_setting_description": "Usar vistas previas embebidas en fotos RAW como entrada para el procesamiento de imágenes y cuando estén disponibles. Esto puede producir colores más precisos en algunas imágenes, pero la calidad de la vista previa depende de la cámara y la imagen puede tener más artefactos de compresión.", "image_prefer_wide_gamut": "Preferir 'gamut' amplio", "image_prefer_wide_gamut_setting_description": "Usar \"Display P3\" para las miniaturas. Preserva mejor la vivacidad de las imágenes con espacios de color amplios pero las imágenes pueden aparecer de manera diferente en dispositivos antiguos con una versión antigua del navegador. Las imágenes sRGB se mantienen como sRGB para evitar cambios de color.", - "image_preview_description": "Imagen de tamaño mediano con metadatos eliminados. Es utilizado al visualizar un solo activo y para el aprendizaje automático", + "image_preview_description": "Imagen de tamaño mediano con metadatos eliminados. Es utilizado al visualizar un solo recurso y para el aprendizaje automático", "image_preview_quality_description": "Calidad de vista previa de 1 a 100. Es mejor cuanto más alta sea la calidad pero genera archivos más grandes y puede reducir la capacidad de respuesta de la aplicación. Establecer un valor bajo puede afectar la calidad del aprendizaje automático.", "image_preview_title": "Ajustes de las vistas previas", "image_progressive": "Progressivo", @@ -134,7 +134,7 @@ "library_scanning_enable_description": "Activar el escaneo periódico de la biblioteca", "library_settings": "Biblioteca externa", "library_settings_description": "Administrar configuración biblioteca externa", - "library_tasks_description": "Buscar elementos nuevos o modificados en bibliotecas externas", + "library_tasks_description": "Buscar recursos nuevos o modificados en bibliotecas externas", "library_updated": "Biblioteca actualizada", "library_watching_enable_description": "Vigilar las bibliotecas externas para detectar cambios en los archivos", "library_watching_settings": "Vigilancia de la biblioteca [EXPERIMENTAL]", @@ -153,7 +153,7 @@ "machine_learning_clip_model_description": "El nombre de un modelo CLIP listado aquí. Tendrás que relanzar el trabajo 'Búsqueda Inteligente' para todos los elementos al cambiar de modelo.", "machine_learning_duplicate_detection": "Detección de duplicados", "machine_learning_duplicate_detection_enabled": "Habilitar detección de duplicados", - "machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, los activos exactamente idénticos seguirán siendo eliminados.", + "machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, los recursos exactamente idénticos seguirán siendo eliminados.", "machine_learning_duplicate_detection_setting_description": "Usa incrustaciones de CLIP (Contrastive Language-Image Pre-Training) para encontrar posibles duplicados", "machine_learning_enabled": "Habilitar aprendizaje automático", "machine_learning_enabled_description": "Al desactivarla todas las funciones de ML se deshabilitarán independientemente de la configuración a continuación.", @@ -224,30 +224,30 @@ "memory_cleanup_job": "Limpieza de recuerdos", "memory_generate_job": "Generación de recuerdos", "metadata_extraction_job": "Extracción de metadatos", - "metadata_extraction_job_description": "Extraer información de metadatos de cada activo, como GPS, caras y resolución", + "metadata_extraction_job_description": "Extraer información de metadatos de cada recurso, como GPS, caras y resolución", "metadata_faces_import_setting": "Activar importación de caras", "metadata_faces_import_setting_description": "Importar caras desde los metadatos EXIF y auxiliares de una imagen", "metadata_settings": "Configuración de metadatos", "metadata_settings_description": "Administrar la configuración de metadatos", "migration_job": "Migración", - "migration_job_description": "Migrar miniaturas de archivos y caras a la estructura de carpetas más reciente", + "migration_job_description": "Migrar miniaturas de recursos y caras a la estructura de carpetas más reciente", "nightly_tasks_cluster_faces_setting_description": "Ejecutar reconocimiento facial en caras detectadas recientemente", "nightly_tasks_cluster_new_faces_setting": "Agrupar caras nuevas", "nightly_tasks_database_cleanup_setting": "Tareas de limpieza de base de datos", "nightly_tasks_database_cleanup_setting_description": "Limpiar datos antiguos y caducados de la base de datos", "nightly_tasks_generate_memories_setting": "Generar recuerdos", - "nightly_tasks_generate_memories_setting_description": "Crear nuevos recuerdos a partir de activos", + "nightly_tasks_generate_memories_setting_description": "Crear nuevos recuerdos a partir de recursos", "nightly_tasks_missing_thumbnails_setting": "Generar miniaturas faltantes", - "nightly_tasks_missing_thumbnails_setting_description": "Poner en cola a activos sin miniaturas para la generación de miniaturas", + "nightly_tasks_missing_thumbnails_setting_description": "Poner en cola recursos sin miniaturas para la generación de miniaturas", "nightly_tasks_settings": "Configuración de Tareas Nocturnas", - "nightly_tasks_settings_description": "Gestionar Tareas Nocturnas", + "nightly_tasks_settings_description": "Gestionar tareas nocturnas", "nightly_tasks_start_time_setting": "Tiempo de inicio", "nightly_tasks_start_time_setting_description": "El tiempo cuando el servidor comienza a ejecutar las tareas nocturnas", "nightly_tasks_sync_quota_usage_setting": "Uso de la cuota de sincronización", "nightly_tasks_sync_quota_usage_setting_description": "Actualizar la cuota de almacenamiento del usuario, según el uso actual", "no_paths_added": "No se han añadido rutas", "no_pattern_added": "No se agregó ningún patrón", - "note_apply_storage_label_previous_assets": "Nota: Para aplicar la Etiqueta de Almacenamiento a los elementos previamente subidos, ejecuta la", + "note_apply_storage_label_previous_assets": "Nota: Para aplicar la Etiqueta de almacenamiento a los recursos previamente subidos, ejecuta la", "note_cannot_be_changed_later": "NOTA: ¡No se puede cambiar posteriormente!", "notification_email_from_address": "Desde", "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server \". Asegúrate de utilizar una dirección desde la que puedas enviar correos electrónicos.", @@ -292,7 +292,7 @@ "oauth_timeout_description": "Tiempo de espera de solicitudes en milisegundos", "ocr_job_description": "Usar aprendizaje automático para reconocer texto en imágenes", "password_enable_description": "Iniciar sesión con correo electrónico y contraseña", - "password_settings": "Contraseña de Acceso", + "password_settings": "Contraseña de inicio de sesión", "password_settings_description": "Administrar la configuración de inicio de sesión con contraseña", "paths_validated_successfully": "Todas las carpetas se han validado satisfactoriamente", "person_cleanup_job": "Limpieza de personas", @@ -311,7 +311,7 @@ "search_jobs": "Buscar trabajos…", "send_welcome_email": "Enviar correo de bienvenida", "server_external_domain_settings": "Dominio externo", - "server_external_domain_settings_description": "Dominio para enlaces públicos compartidos, incluidos http(s)://", + "server_external_domain_settings_description": "Dominio usado para enlaces externos", "server_public_users": "Usuarios públicos", "server_public_users_description": "Cuando se añade un usuario a los álbumes compartidos, todos los usuarios aparecen en una lista con su nombre y su correo electrónico. Si deshabilita esta opción, solo los administradores podrán ver la lista de usuarios.", "server_settings": "Configuración del servidor", @@ -323,15 +323,15 @@ "sidecar_job": "Metadatos de archivos sidecar", "sidecar_job_description": "Descubrir o sincronizar metadatos sidecar desde el sistema de archivos", "slideshow_duration_description": "Número de segundos para mostrar cada imagen", - "smart_search_job_description": "Ejecute aprendizaje automático en archivos para respaldar la búsqueda inteligente", - "storage_template_date_time_description": "La fecha y hora de creación del elemento será usada para la información sobre la fecha", + "smart_search_job_description": "Ejecute aprendizaje automático en recursos para respaldar la búsqueda inteligente", + "storage_template_date_time_description": "La fecha y hora de creación del recurso será usada para la información sobre la fecha", "storage_template_date_time_sample": "Hora de la muestra {date}", "storage_template_enable_description": "Habilitar el motor de plantillas de almacenamiento", "storage_template_hash_verification_enabled": "Verificación de hash habilitada", "storage_template_hash_verification_enabled_description": "Habilita la verificación de hash, no la desactive a menos que esté seguro de las implicaciones", "storage_template_migration": "Migración de plantillas de almacenamiento", - "storage_template_migration_description": "Aplicar la {template} actual a los elementos subidos previamente", - "storage_template_migration_info": "La plantilla de almacenamiento convertirá todas las extensiones a minúscula. Los cambios en las plantillas solo se aplican a los elementos nuevos. Para aplicarlos retroactivamente a los elementos subidos previamente ejecute la {job}.", + "storage_template_migration_description": "Aplicar la {template} actual a los recursos subidos previamente", + "storage_template_migration_info": "La plantilla de almacenamiento convertirá todas las extensiones a minúscula. Los cambios en las plantillas solo se aplican a los recursos nuevos. Para aplicarlos retroactivamente a los recursos subidos previamente ejecute la {job}.", "storage_template_migration_job": "Tarea de migración de la plantilla de almacenamiento", "storage_template_more_details": "Para obtener más detalles sobre esta función, consulte la Plantilla de almacenamiento y sus implicaciones", "storage_template_onboarding_description_v2": "Al habilitar esta función, los archivos se organizarán automáticamente según la plantilla definida por el usuario. Para más información, consulte la documentación.", @@ -350,13 +350,13 @@ "template_email_welcome": "Plantilla de correo electrónico de bienvenida", "template_settings": "Plantillas de notificación", "template_settings_description": "Gestione plantillas personalizadas para las notificaciones", - "theme_custom_css_settings": "CSS Personalizado", - "theme_custom_css_settings_description": "Las Hojas de Estilo (CSS) permiten personalizar el diseño de Immich.", + "theme_custom_css_settings": "CSS personalizado", + "theme_custom_css_settings_description": "El CSS permite personalizar el diseño de Immich.", "theme_settings": "Ajustes del tema", "theme_settings_description": "Gestionar la personalización de la interfaz web de Immich", "thumbnail_generation_job": "Generar miniaturas", - "thumbnail_generation_job_description": "Genere miniaturas grandes, pequeñas y borrosas para cada archivo, así como miniaturas para cada persona", - "transcoding_acceleration_api": "API Aceleración", + "thumbnail_generation_job_description": "Genere miniaturas grandes, pequeñas y borrosas para cada recurso, así como miniaturas para cada persona", + "transcoding_acceleration_api": "API de aceleración", "transcoding_acceleration_api_description": "La API que interactuará con su dispositivo para acelerar la transcodificación. Esta configuración es el \"mejor esfuerzo\": recurrirá a la transcodificación del software en caso de error. VP9 puede funcionar o no dependiendo de su hardware.", "transcoding_acceleration_nvenc": "NVENC (requiere GPU NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (requiere procesador Intel de 7ª generación o superior)", @@ -380,7 +380,7 @@ "transcoding_disabled_description": "No transcodifique ningún vídeo; puede interrumpir la reproducción en algunos clientes", "transcoding_encoding_options": "Opciones de codificación", "transcoding_encoding_options_description": "Establecer códecs, resolución, calidad y otras opciones para los vídeos codificados", - "transcoding_hardware_acceleration": "Aceleración por Hardware", + "transcoding_hardware_acceleration": "Aceleración por hardware", "transcoding_hardware_acceleration_description": "Experimental: transcodificación más rápida, pero puede reducir la calidad con la misma tasa de bits", "transcoding_hardware_decoding": "Decodificación por hardware", "transcoding_hardware_decoding_setting_description": "Permite la aceleración de extremo a extremo en lugar de acelerar únicamente la codificación. Puede que no funcione en todos los vídeos.", @@ -400,7 +400,7 @@ "transcoding_reference_frames": "Frames de referencia", "transcoding_reference_frames_description": "El número de fotogramas a los que hacer referencia al comprimir un fotograma determinado. Los valores más altos mejoran la eficiencia de la compresión, pero ralentizan la codificación. 0 establece este valor automáticamente.", "transcoding_required_description": "Sólo vídeos que no estén en un formato soportado", - "transcoding_settings": "Configuración de Transcodificación de Vídeo", + "transcoding_settings": "Configuración de transcodificación de vídeo", "transcoding_settings_description": "Administrar qué vídeos transcodificar y cómo procesarlos", "transcoding_target_resolution": "Resolución deseada", "transcoding_target_resolution_description": "Las resoluciones más altas pueden conservar más detalles, pero la codificación tarda más, tienen tamaños de archivo más grandes y pueden reducir la capacidad de respuesta de la aplicación.", @@ -414,22 +414,22 @@ "transcoding_transcode_policy_description": "Política sobre cuándo se debe transcodificar un vídeo. Los vídeos HDR siempre se transcodificarán (excepto si la transcodificación está desactivada).", "transcoding_two_pass_encoding": "Codificación en dos pasadas", "transcoding_two_pass_encoding_setting_description": "Transcodifica en dos pasadas para producir vídeos mejor codificados. Cuando la velocidad de bits máxima está habilitada (es necesaria para que funcione con H.264 y HEVC), este modo utiliza un rango de velocidad de bits basado en la velocidad de bits máxima e ignora CRF. Para VP9, se puede utilizar CRF si la tasa de bits máxima está deshabilitada.", - "transcoding_video_codec": "Códecs de Video", + "transcoding_video_codec": "Códecs de video", "transcoding_video_codec_description": "VP9 tiene alta eficiencia y compatibilidad web, pero lleva mucho tiempo transcodificarlo. HEVC ofrece un rendimiento similar, pero tiene menor compatibilidad web. H.264 es ampliamente compatible y se transcodifica muy rápido, pero los archivos producidos son mucho más grandes. AV1 es el códec más eficiente, pero no es compatible con los dispositivos más antiguos.", "trash_enabled_description": "Habilitar papelera", "trash_number_of_days": "Número de días", - "trash_number_of_days_description": "Número de días para mantener los archivos en la papelera antes de eliminarlos permanentemente", + "trash_number_of_days_description": "Número de días para mantener los recursos en la papelera antes de eliminarlos permanentemente", "trash_settings": "Configuración papelera", "trash_settings_description": "Administrar la configuración de la papelera", "unlink_all_oauth_accounts": "Desvincular todas las cuentas de OAuth", "unlink_all_oauth_accounts_description": "Recuerda desvincular todas las cuentas de OAuth antes de migrar a un proveedor nuevo.", "unlink_all_oauth_accounts_prompt": "¿Seguro que deseas desvincular todas las cuentas de OAuth? Se restablecerá el id. de OAuth de cada usuario. La acción no se podrá deshacer.", "user_cleanup_job": "Limpieza de usuarios", - "user_delete_delay": "La cuenta {user} y los archivos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.", + "user_delete_delay": "La cuenta {user} y los recursos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.", "user_delete_delay_settings": "Eliminar retardo", - "user_delete_delay_settings_description": "Número de días después de la eliminación para eliminar permanentemente la cuenta y los activos de un usuario. El trabajo de eliminación de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estén listos para su eliminación. Los cambios a esta configuración se evaluarán en la próxima ejecución.", - "user_delete_immediately": "La cuenta {user} y los archivos se pondrán en cola para su eliminación permanente inmediatamente.", - "user_delete_immediately_checkbox": "Poner en cola la eliminación inmediata de usuarios y elementos", + "user_delete_delay_settings_description": "Número de días después de la eliminación para eliminar permanentemente la cuenta y los recursos de un usuario. El trabajo de eliminación de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estén listos para su eliminación. Los cambios a esta configuración se evaluarán en la próxima ejecución.", + "user_delete_immediately": "La cuenta {user} y los recursos se pondrán en cola para su eliminación permanente inmediatamente.", + "user_delete_immediately_checkbox": "Poner en cola la eliminación inmediata de usuarios y recursos", "user_details": "Detalles del usuario", "user_management": "Gestión de usuarios", "user_password_has_been_reset": "La contraseña del usuario ha sido restablecida:", @@ -442,7 +442,7 @@ "users_page_description": "Página de usuarios administradores", "version_check_enabled_description": "Activar la comprobación de la versión", "version_check_implications": "La función de comprobación de versiones depende de la comunicación periódica con github.com", - "version_check_settings": "Verificar Versión", + "version_check_settings": "Verificar versión", "version_check_settings_description": "Activar/desactivar la notificación de nueva versión", "video_conversion_job": "Transcodificar vídeos", "video_conversion_job_description": "Transcodifique vídeos para una mayor compatibilidad con navegadores y dispositivos" @@ -457,7 +457,7 @@ "advanced_settings_enable_alternate_media_filter_subtitle": "Usa esta opción para filtrar medios durante la sincronización según criterios alternativos. Intenta esto solo si tienes problemas con que la aplicación detecte todos los álbumes.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronización de álbumes del dispositivo", "advanced_settings_log_level_title": "Nivel de registro: {level}", - "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas desde los archivos locales. 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 desde los recursos locales. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", "advanced_settings_proxy_headers_subtitle": "Configura encabezados HTTP que Immich incluirá en cada petición de red", "advanced_settings_proxy_headers_title": "Cabeceras proxy personalizadas [EXPERIMENTAL]", @@ -485,26 +485,26 @@ "album_info_updated": "Información del álbum actualizada", "album_leave": "¿Abandonar el álbum?", "album_leave_confirmation": "¿Estás seguro de que quieres dejar {album}?", - "album_name": "Nombre del Álbum", - "album_options": "Opciones del Album", + "album_name": "Nombre del álbum", + "album_options": "Opciones del álbum", "album_remove_user": "¿Eliminar usuario?", "album_remove_user_confirmation": "¿Estás seguro de que quieres eliminar a {user}?", "album_search_not_found": "No se encontraron álbumes que coincidan con tu búsqueda", "album_selected": "Álbum seleccionado", "album_share_no_users": "Parece que has compartido este álbum con todos los usuarios o no tienes ningún usuario con quien compartirlo.", "album_summary": "Resumen del álbum", - "album_updated": "Album actualizado", - "album_updated_setting_description": "Reciba una notificación por correo electrónico cuando un álbum compartido tenga nuevos archivos", + "album_updated": "Álbum actualizado", + "album_updated_setting_description": "Reciba una notificación por correo electrónico cuando un álbum compartido tenga nuevos recursos", "album_upload_assets": "Añadir recursos desde tu computadora y añadir a un álbum", - "album_user_left": "Salida {album}", + "album_user_left": "Abandonó {album}", "album_user_removed": "Eliminado a {user}", "album_viewer_appbar_delete_confirm": "¿Estás seguro/a que quieres borrar este álbum de tu cuenta?", "album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum", "album_viewer_appbar_share_err_leave": "No se ha podido abandonar el álbum", - "album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los elementos del álbum", + "album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los recursos del álbum", "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum", "album_viewer_appbar_share_leave": "Abandonar álbum", - "album_viewer_appbar_share_to": "Compartir Con", + "album_viewer_appbar_share_to": "Compartir con", "album_viewer_page_share_add_users": "Añadir usuarios", "album_with_link_access": "Permitir que cualquiera que tenga el enlace vea las fotos y las personas en este álbum.", "albums": "Álbumes", @@ -537,78 +537,78 @@ "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?", "app_bar_signout_dialog_ok": "Sí", "app_bar_signout_dialog_title": "Cerrar sesión", - "app_download_links": "Enlaces de Descarga de la Aplicación", - "app_settings": "Ajustes de la aplicacion", - "app_stores": "Tiendas de Aplicaciones", + "app_download_links": "Enlaces de descarga de la aplicación", + "app_settings": "Ajustes de la aplicación", + "app_stores": "Tiendas de aplicaciones", "app_update_available": "Actualización de aplicación está disponible", "appears_in": "Aparece en", "apply_count": "Aplicar ({count, number})", "archive": "Archivo", "archive_action_prompt": "{count} añadido(s) al archivo", "archive_or_unarchive_photo": "Archivar o restaurar foto", - "archive_page_no_archived_assets": "No se encontraron elementos archivados", + "archive_page_no_archived_assets": "No se encontraron recursos archivados", "archive_page_title": "Archivo ({count})", "archive_size": "Tamaño de archivo comprimido", - "archive_size_description": "Configure el tamaño del archivo para descargas (en GB)", + "archive_size_description": "Configure el tamaño del archivo para descargas (en GiB)", "archived": "Archivado", "archived_count": "{count, plural, one {# archivado} other {# archivados}}", "are_these_the_same_person": "¿Son la misma persona?", "are_you_sure_to_do_this": "¿Estás seguro de que quieres hacer esto?", "array_field_not_fully_supported": "Los campos de la matriz requieren edición manual de JSON", - "asset_action_delete_err_read_only": "No se puede borrar archivo(s) de solo lectura, omitiendo", - "asset_action_share_err_offline": "No se pudo obtener archivo(s) sin conexión, omitiendo", + "asset_action_delete_err_read_only": "No se puede borrar recurso(s) de solo lectura, omitiendo", + "asset_action_share_err_offline": "No se pudo obtener recurso(s) sin conexión, omitiendo", "asset_added_to_album": "Añadido al álbum", "asset_adding_to_album": "Añadiendo al álbum…", - "asset_created": "Activo creado", - "asset_description_updated": "La descripción del elemento ha sido actualizada", - "asset_filename_is_offline": "El archivo {filename} está offline", - "asset_has_unassigned_faces": "El archivo no tiene rostros asignados", + "asset_created": "Recurso creado", + "asset_description_updated": "La descripción del recurso ha sido actualizada", + "asset_filename_is_offline": "El recurso {filename} está desconectado", + "asset_has_unassigned_faces": "El recurso no tiene rostros asignados", "asset_hashing": "Calculando hash…", "asset_list_group_by_sub_title": "Agrupar por", "asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico", - "asset_list_layout_settings_group_automatically": "Automatico", - "asset_list_layout_settings_group_by": "Agrupar elementos por", + "asset_list_layout_settings_group_automatically": "Automático", + "asset_list_layout_settings_group_by": "Agrupar recursos por", "asset_list_layout_settings_group_by_month_day": "Mes + día", "asset_list_layout_sub_title": "Disposición", "asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos", "asset_list_settings_title": "Cuadrícula de fotos", - "asset_not_found_on_device_android": "Activo no encontrado en el dispositivo", + "asset_not_found_on_device_android": "Recurso no encontrado en el dispositivo", "asset_not_found_on_device_ios": "No se encuentra el recurso en el dispositivo. Si usa iCloud, es posible que no pueda acceder al recurso debido a un archivo defectuoso almacenado en iCloud", "asset_not_found_on_icloud": "No se ha encontrado el recurso en iCloud. Es posible que no se pueda acceder al recurso debido a un archivo defectuoso almacenado en iCloud", - "asset_offline": "Archivos sin conexión", - "asset_offline_description": "Este activo externo ya no se encuentra en el disco. Por favor, póngase en contacto con su administrador de Immich para obtener ayuda.", - "asset_restored_successfully": "Elementos restaurados exitosamente", + "asset_offline": "Recurso sin conexión", + "asset_offline_description": "Este recurso externo ya no se encuentra en el disco. Por favor, póngase en contacto con su administrador de Immich para obtener ayuda.", + "asset_restored_successfully": "Recursos restaurados exitosamente", "asset_skipped": "Omitido", "asset_skipped_in_trash": "En la papelera", - "asset_trashed": "Elemento eliminado", - "asset_troubleshoot": "Diagnóstico del elemento", + "asset_trashed": "Recurso eliminado", + "asset_troubleshoot": "Diagnóstico del recurso", "asset_uploaded": "Subido", "asset_uploading": "Subiendo…", "asset_viewer_settings_subtitle": "Administra las configuraciones de tu visor de fotos", - "asset_viewer_settings_title": "Visor de archivos", - "assets": "elementos", - "assets_added_count": "{count, plural, one {# elemento añadido} other {# elementos añadidos}}", - "assets_added_to_album_count": "{count, plural, one {# elemento añadido} other {# elementos añadidos}} al álbum", + "asset_viewer_settings_title": "Visor de recursos", + "assets": "Recursos", + "assets_added_count": "{count, plural, one {# recurso añadido} other {# recursos añadidos}}", + "assets_added_to_album_count": "{count, plural, one {# recurso añadido} other {# recurso añadidos}} al álbum", "assets_added_to_albums_count": "{assetTotal, plural, one {# añadido} other {# añadidos}} {albumTotal, plural, one {# al álbum} other {# a los álbumes}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {El elemento no se puede añadir al álbum} other {Los elementos no se pueden añadir al álbum}}", - "assets_cannot_be_added_to_albums": "{count, plural, one {El elemento} other {Los elementos}} no se {count, plural, one {puede} other {pueden}} añadir a ninguno de los álbumes", - "assets_count": "{count, plural, one {# activo} other {# activos}}", - "assets_deleted_permanently": "{count} elemento(s) eliminado(s) permanentemente", + "assets_cannot_be_added_to_album_count": "{count, plural, one {El recurso no se puede añadir al álbum} other {Los recursos no se pueden añadir al álbum}}", + "assets_cannot_be_added_to_albums": "{count, plural, one {El recurso} other {Los recursos}} no se {count, plural, one {puede} other {pueden}} añadir a ninguno de los álbumes", + "assets_count": "{count, plural, one {# recurso} other {# recursos}}", + "assets_deleted_permanently": "{count} recurso(s) eliminado(s) permanentemente", "assets_deleted_permanently_from_server": "{count} recurso(s) eliminado(s) de forma permanente del servidor de Immich", "assets_downloaded_failed": "{count, plural, one {# archivo descargado - {error} archivo fallido} other {# archivos descargados - {error} archivos fallidos}}", "assets_downloaded_successfully": "{count, plural, one {# archivo descargado exitosamente} other {# archivos descargados exitosamente}}", - "assets_moved_to_trash_count": "{count, plural, one {# elemento movido} other {# elementos movidos}} a la papelera", - "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", - "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}", - "assets_removed_permanently_from_device": "{count} elemento(s) eliminado(s) permanentemente de su dispositivo", - "assets_restore_confirmation": "¿Estás seguro de que quieres restaurar todos tus activos eliminados? ¡No puede deshacer esta acción! Tenga en cuenta que los archivos sin conexión no se pueden restaurar de esta manera.", - "assets_restored_count": "Restaurado {count, plural, one {# elemento} other {# elementos}}", - "assets_restored_successfully": "{count} elemento(s) restaurado(s) exitosamente", - "assets_trashed": "{count} elemento(s) eliminado(s)", - "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", + "assets_moved_to_trash_count": "{count, plural, one {# recurso movido} other {# recursos movidos}} a la papelera", + "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# recurso} other {# recursos}}", + "assets_removed_count": "Eliminado {count, plural, one {# recurso} other {# recursos}}", + "assets_removed_permanently_from_device": "{count} recurso(s) eliminado(s) permanentemente de su dispositivo", + "assets_restore_confirmation": "¿Estás seguro de que quieres restaurar todos tus recursos eliminados? ¡No puede deshacer esta acción! Tenga en cuenta que los recursos sin conexión no se pueden restaurar de esta manera.", + "assets_restored_count": "Restaurado {count, plural, one {# recurso} other {# recursos}}", + "assets_restored_successfully": "{count} recurso(s) restaurado(s) exitosamente", + "assets_trashed": "{count} recurso(s) eliminado(s)", + "assets_trashed_count": "Borrado {count, plural, one {# recurso} other {# recursos}}", "assets_trashed_from_server": "{count} recurso(s) enviado(s) a la papelera desde el servidor de Immich", - "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum", - "assets_were_part_of_albums_count": "{count, plural, one {El elemento ya es} other {Los elementos ya son}} parte de los álbumes", + "assets_were_part_of_album_count": "{count, plural, one {El recurso ya forma} other {Los recursos ya forman}} parte del álbum", + "assets_were_part_of_albums_count": "{count, plural, one {El recurso ya es} other {Los recursos ya son}} parte de los álbumes", "authorized_devices": "Dispositivos autorizados", "automatic_endpoint_switching_subtitle": "Conectarse localmente a través de la Wi-Fi designada cuando esté disponible y usar conexiones alternativas en otros lugares", "automatic_endpoint_switching_title": "Cambio automático de URL", @@ -622,47 +622,47 @@ "backup": "Copia de seguridad", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", - "backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", + "backup_album_selection_page_assets_scatter": "Los recursos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", "backup_album_selection_page_select_albums": "Seleccionar álbumes", - "backup_album_selection_page_selection_info": "Información sobre la Selección", - "backup_album_selection_page_total_assets": "Total de elementos únicos", + "backup_album_selection_page_selection_info": "Información sobre la selección", + "backup_album_selection_page_total_assets": "Total de recursos únicos", "backup_albums_sync": "Sincronización de álbumes de respaldo", "backup_all": "Todos", - "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando…", - "backup_background_service_complete_notification": "Copia de seguridad de activos completada", + "backup_background_service_backup_failed_message": "Error al copiar recursos. Reintentando…", + "backup_background_service_complete_notification": "Copia de seguridad de recursos completada", "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…", "backup_background_service_current_upload_notification": "Subiendo {filename}", - "backup_background_service_default_notification": "Comprobando nuevos elementos…", + "backup_background_service_default_notification": "Comprobando nuevos recursos…", "backup_background_service_error_title": "Error de copia de seguridad", - "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos…", + "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus recursos…", "backup_background_service_upload_failure_notification": "Error al subir {filename}", "backup_controller_page_albums": "Álbumes de copia de seguridad", "backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.", "backup_controller_page_background_app_refresh_disabled_title": "Actualización en segundo plano desactivada", "backup_controller_page_background_app_refresh_enable_button_text": "Ir a configuración", - "backup_controller_page_background_battery_info_link": "Muestrame cómo", + "backup_controller_page_background_battery_info_link": "Muéstrame cómo", "backup_controller_page_background_battery_info_message": "Para obtener la mejor experiencia de copia de seguridad en segundo plano, desactiva cualquier optimización de batería que restrinja la actividad en segundo plano para Immich.\n\nDado que esto es específico en cada dispositivo, busca la información necesaria de el fabricante de tu dispositivo.", - "backup_controller_page_background_battery_info_ok": "Ok", + "backup_controller_page_background_battery_info_ok": "Aceptar", "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", "backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", - "backup_controller_page_background_delay": "Retrasar la copia de seguridad de los nuevos elementos: {duration}", - "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos elementos sin necesidad de abrir la aplicación", + "backup_controller_page_background_delay": "Retrasar la copia de seguridad de los nuevos recursos: {duration}", + "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier recurso nuevo sin necesidad de abrir la aplicación", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está activada", "backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano", "backup_controller_page_background_turn_on": "Activar el servicio en segundo plano", "backup_controller_page_background_wifi": "Solo en Wi-Fi", - "backup_controller_page_backup": "Copia de Seguridad", + "backup_controller_page_backup": "Copia de seguridad", "backup_controller_page_backup_selected": "Seleccionado: ", "backup_controller_page_backup_sub": "Fotos y videos respaldados", "backup_controller_page_created": "Creado el: {date}", - "backup_controller_page_desc_backup": "Active la copia de seguridad para subir automáticamente los nuevos elementos al servidor cuando se abre la aplicación.", + "backup_controller_page_desc_backup": "Active la copia de seguridad para subir automáticamente los nuevos recursos al servidor cuando se abre la aplicación.", "backup_controller_page_excluded": "Excluido: ", "backup_controller_page_failed": "Fallidos ({count})", "backup_controller_page_filename": "Nombre del archivo: {filename} [{size}]", "backup_controller_page_id": "Id.: {id}", - "backup_controller_page_info": "Información de la Copia de Seguridad", + "backup_controller_page_info": "Información de la copia de seguridad", "backup_controller_page_none_selected": "Ninguno seleccionado", "backup_controller_page_remainder": "Restante", "backup_controller_page_remainder_sub": "Fotos y videos restantes para hacer una copia de seguridad de la selección", @@ -678,13 +678,13 @@ "backup_controller_page_uploading_file_info": "Subiendo información del archivo", "backup_err_only_album": "No se puede eliminar el único álbum", "backup_error_sync_failed": "La sincronización falló. No es posible procesar la copia de seguridad.", - "backup_info_card_assets": "elementos", + "backup_info_card_assets": "recursos", "backup_manual_cancelled": "Cancelado", "backup_manual_in_progress": "Subida ya en progreso. Vuelve a intentarlo más tarde", "backup_manual_success": "Éxito", "backup_manual_title": "Estado de la subida", "backup_options": "Opciones de copia de seguridad", - "backup_options_page_title": "Opciones de Copia de Seguridad", + "backup_options_page_title": "Opciones de copia de seguridad", "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano", "backup_settings_subtitle": "Configura las opciones de subida", "backup_upload_details_page_more_details": "Toca para más detalles", @@ -699,15 +699,15 @@ "bugs_and_feature_requests": "Errores y solicitudes de funciones", "build": "Compilación", "build_image": "Imagen de compilación", - "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", - "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.", - "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.", + "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# recurso duplicado} other {# recursos duplicados}}? Esto mantendrá el recurso más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", + "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# recurso duplicado} other {# recursos duplicados}}? Esto resolverá todos los grupos duplicados sin borrar nada.", + "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# recurso duplicado} other {# recursos duplicados}}? Esto mantendrá el recurso más grande de cada grupo y eliminará todos los demás duplicados.", "buy": "Comprar Immich", "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_duplicated_assets_clear_button": "LIMPIAR", "cache_settings_duplicated_assets_subtitle": "Fotos y vídeos ignorados por la aplicación", - "cache_settings_duplicated_assets_title": "Elementos duplicados ({count})", + "cache_settings_duplicated_assets_title": "Recursos duplicados ({count})", "cache_settings_statistics_album": "Miniaturas de la biblioteca", "cache_settings_statistics_full": "Imágenes completas", "cache_settings_statistics_shared": "Miniaturas de álbumes compartidos", @@ -752,23 +752,23 @@ "changed_visibility_successfully": "Visibilidad cambiada correctamente", "charging": "Cargando", "charging_requirement_mobile_backup": "La copia de seguridad en segundo plano requiere que el dispositivo se esté cargando", - "check_corrupt_asset_backup": "Comprobar copias de seguridad de archivos corruptos", + "check_corrupt_asset_backup": "Comprobar copias de seguridad de recursos corruptos", "check_corrupt_asset_backup_button": "Realizar comprobación", - "check_corrupt_asset_backup_description": "Ejecutar esta comprobación solo por Wi-Fi y una vez que todos los archivos hayan sido respaldados. El procedimiento puede tardar unos minutos.", + "check_corrupt_asset_backup_description": "Ejecutar esta comprobación solo por Wi-Fi y una vez que todos los recursos hayan sido respaldados. El procedimiento puede tardar unos minutos.", "check_logs": "Comprobar Registros", "checksum": "Suma de comprobación", "choose_matching_people_to_merge": "Elija ocurrencias duplicadas de la misma persona para fusionar", "city": "Ciudad", "cleanup_confirm_description": "Immich encontró {count} recursos (creados antes de {date}) respaldados de manera segura en el servidor. ¿Desea eliminar las copias locales de este dispositivo?", - "cleanup_confirm_prompt_title": "¿Remover de este dispositivo?", - "cleanup_deleted_assets": "Moviendo {count} elementos del dispositivo a la papelera", + "cleanup_confirm_prompt_title": "¿Eliminar de este dispositivo?", + "cleanup_deleted_assets": "Moviendo {count} recursos del dispositivo a la papelera", "cleanup_deleting": "Moviendo a la papelera...", - "cleanup_found_assets": "Se han encontrado {count} archivos respaldados", - "cleanup_found_assets_with_size": "Se encontraron {count} activos respaldados ({size})", + "cleanup_found_assets": "Se han encontrado {count} recursos respaldados", + "cleanup_found_assets_with_size": "Se encontraron {count} recursos respaldados ({size})", "cleanup_icloud_shared_albums_excluded": "Los álbumes compartidos de iCloud están excluidos del escaneo", - "cleanup_no_assets_found": "No se encontraron activos que coincidan con los criterios anteriores. Liberar espacio solo puede eliminar activos respaldados en el servidor", - "cleanup_preview_title": "{count} archivos a remover", - "cleanup_step3_description": "Busque activos respaldados que coincidan con su fecha y conserve la configuración.", + "cleanup_no_assets_found": "No se encontraron recursos que coincidan con los criterios anteriores. Liberar espacio solo puede eliminar recursos respaldados en el servidor", + "cleanup_preview_title": "{count} recursos a remover", + "cleanup_step3_description": "Busque recursos respaldados que coincidan con su fecha y conserve la configuración.", "cleanup_step4_summary": "{count} recursos (creados antes del {date}) para eliminar de tu dispositivo local. Las fotos seguirán accesibles desde la app de Immich.", "cleanup_trash_hint": "Para completar la liberación de espacio, abra la aplicación de fotos y vacíe la papelera", "clear": "Limpiar", @@ -778,10 +778,12 @@ "clear_message": "Limpiar mensaje", "clear_value": "Limpiar valor", "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Introduzca contraseña", + "client_cert_enter_password": "Introduzca la contraseña", "client_cert_import": "Importar", "client_cert_import_success_msg": "El certificado de cliente está importado", "client_cert_invalid_msg": "Archivo de certificado no válido o contraseña incorrecta", + "client_cert_password_message": "Introduzca la contraseña para este certificado", + "client_cert_password_title": "Contraseña del certificado", "client_cert_remove_msg": "El certificado de cliente se ha eliminado", "client_cert_subtitle": "Solo se admite el formato PKCS12 (.p12, .pfx). La importación/eliminación de certificados solo está disponible antes de iniciar sesión", "client_cert_title": "Certificado de cliente SSL [EXPERIMENTAL]", @@ -792,6 +794,11 @@ "color": "Color", "color_theme": "Color del tema", "command": "Comando", + "command_palette_prompt": "Encuentra rápidamente páginas, acciones o comandos", + "command_palette_to_close": "para cerrar", + "command_palette_to_navigate": "para entrar", + "command_palette_to_select": "para seleccionar", + "command_palette_to_show_all": "para mostrar todo", "comment_deleted": "Comentario borrado", "comment_options": "Opciones de comentarios", "comments_and_likes": "Comentarios y me gusta", @@ -800,9 +807,9 @@ "completed": "Completado", "confirm": "Confirmar", "confirm_admin_password": "Confirmar contraseña del administrador", - "confirm_delete_face": "¿Estás seguro que deseas eliminar la cara de {name} del archivo?", + "confirm_delete_face": "¿Estás seguro que deseas eliminar la cara de {name} del recurso?", "confirm_delete_shared_link": "¿Estás seguro de que deseas eliminar este enlace compartido?", - "confirm_keep_this_delete_others": "Todos los demás activos de la pila se eliminarán excepto este activo. ¿Está seguro de que quiere continuar?", + "confirm_keep_this_delete_others": "Todos los demás recursos de la pila se eliminarán excepto este recurso. ¿Está seguro de que quiere continuar?", "confirm_new_pin_code": "Confirmar nuevo PIN", "confirm_password": "Confirmar contraseña", "confirm_tag_face": "¿Quieres etiquetar esta cara como {name}?", @@ -828,7 +835,7 @@ "copy_link": "Copiar enlace", "copy_link_to_clipboard": "Copiar enlace al portapapeles", "copy_password": "Copiar contraseña", - "copy_to_clipboard": "Copiar al Portapapeles", + "copy_to_clipboard": "Copiar al portapapeles", "country": "País", "cover": "Portada", "covers": "Portadas", @@ -841,11 +848,11 @@ "create_link": "Crear enlace", "create_link_to_share": "Crear enlace compartido", "create_link_to_share_description": "Permitir que cualquier persona con el enlace vea la(s) foto(s) seleccionada(s)", - "create_new": "Crear nuevo", + "create_new": "CREAR NUEVO", "create_new_person": "Crear nueva persona", - "create_new_person_hint": "Asignar los archivos seleccionados a una nueva persona", + "create_new_person_hint": "Asignar los recursos seleccionados a una nueva persona", "create_new_user": "Crear nuevo usuario", - "create_shared_album_page_share_add_assets": "AÑADIR ELEMENTOS", + "create_shared_album_page_share_add_assets": "AÑADIR RECURSOS", "create_shared_album_page_share_select_photos": "Seleccionar fotos", "create_shared_link": "Crear un enlace compartido", "create_tag": "Crear etiqueta", @@ -876,7 +883,7 @@ "dark_theme": "Alternar tema oscuro", "date": "Fecha", "date_after": "Fecha posterior", - "date_and_time": "Fecha y Hora", + "date_and_time": "Fecha y hora", "date_before": "Fecha anterior", "date_format": "E d, LLL y • h:mm a", "date_of_birth_saved": "Guardada con éxito la fecha de nacimiento", @@ -891,7 +898,7 @@ "default_locale": "Configuración regional predeterminada", "default_locale_description": "Formatee fechas y números según la configuración regional de su navegador", "delete": "Eliminar", - "delete_action_confirmation_message": "¿Está seguro que desea eliminar este archivo? Esta acción lo moverá a la papelera del servidor y le preguntará si desea eliminarlo localmente", + "delete_action_confirmation_message": "¿Está seguro que desea eliminar este recurso? Esta acción lo moverá a la papelera del servidor y le preguntará si desea eliminarlo localmente", "delete_action_prompt": "{count} eliminados", "delete_album": "Eliminar álbum", "delete_api_key_prompt": "¿Está seguro de que desea eliminar esta clave API?", @@ -900,7 +907,7 @@ "delete_dialog_alert_local_non_backed_up": "Algunos de los elementos no tienen copia de seguridad en Immich y serán borrados permanentemente de tu dispositivo", "delete_dialog_alert_remote": "Estas imágenes van a ser borradas permanentemente del servidor de Immich", "delete_dialog_ok_force": "Borrar de todos modos", - "delete_dialog_title": "Eliminar Permanentemente", + "delete_dialog_title": "Eliminar permanentemente", "delete_duplicates_confirmation": "¿Está seguro de que desea eliminar permanentemente estos duplicados?", "delete_face": "Eliminar cara", "delete_key": "Eliminar clave", @@ -918,11 +925,11 @@ "delete_tag_confirmation_prompt": "¿Estás seguro de que deseas eliminar la etiqueta {tagName} ?", "delete_user": "Eliminar usuario", "deleted_shared_link": "Enlace compartido eliminado", - "deletes_missing_assets": "Elimina archivos que faltan en el disco duro", + "deletes_missing_assets": "Elimina recursos que faltan en el disco duro", "description": "Descripción", "description_input_hint_text": "Añadir descripción...", "description_input_submit_error": "Error al actualizar la descripción, comprueba el registro para obtener más detalles", - "deselect_all": "Deseleccionar Todo", + "deselect_all": "Deseleccionar todo", "details": "Detalles", "direction": "Dirección", "disable": "Desactivar", @@ -936,12 +943,12 @@ "display_options": "Opciones de pantalla", "display_order": "Orden de visualización", "display_original_photos": "Mostrar fotos originales", - "display_original_photos_setting_description": "Preferir mostrar la foto original al ver un archivo en lugar de miniaturas cuando el archivo original es compatible con la web. Esto puede resultar en velocidades de visualización de fotografías más lentas.", + "display_original_photos_setting_description": "Preferir mostrar la foto original al ver un recurso en lugar de miniaturas cuando el recurso original es compatible con la web. Esto puede resultar en velocidades de visualización de fotografías más lentas.", "do_not_show_again": "No volver a mostrar este mensaje otra vez", "documentation": "Documentación", "done": "Hecho", "download": "Descargar", - "download_action_prompt": "Descargando {count} archivos", + "download_action_prompt": "Descargando {count} recursos", "download_canceled": "Descarga cancelada", "download_complete": "Descarga completada", "download_enqueue": "Descarga en cola", @@ -954,13 +961,13 @@ "download_original": "Descargar original", "download_paused": "Descarga en pausa", "download_settings": "Descargar", - "download_settings_description": "Administrar configuraciones relacionadas con la descarga de archivos", + "download_settings_description": "Administrar configuraciones relacionadas con la descarga de recursos", "download_started": "Descarga iniciada", - "download_sucess": "Descarga Exitosa", + "download_sucess": "Descarga exitosa", "download_sucess_android": "Los archivos se han descargado en DCIM/Immich", "download_waiting_to_retry": "Esperando para reintentar", "downloading": "Descargando", - "downloading_asset_filename": "Descargando archivo {filename}", + "downloading_asset_filename": "Descargando recurso {filename}", "downloading_from_icloud": "Descargando desde iCloud", "downloading_media": "Descargando medios", "drop_files_to_upload": "Suelta los archivos en cualquier lugar para subirlos", @@ -968,7 +975,7 @@ "duplicates_description": "Resuelva cada grupo indicando, en cada caso, cuales están duplicados", "duration": "Duración", "edit": "Editar", - "edit_album": "Editar album", + "edit_album": "Editar álbum", "edit_avatar": "Editar avatar", "edit_birthday": "Editar cumpleaños", "edit_date": "Editar fecha", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "No se guardarán los cambios", "editor_close_without_save_title": "¿Cerrar el editor?", "editor_confirm_reset_all_changes": "¿Seguro que quieres restablecer los cambios?", + "editor_discard_edits_confirm": "Descartar ediciones", + "editor_discard_edits_prompt": "Tiene ediciones sin guardar. ¿Está seguro de que quieres descartarlas?", + "editor_discard_edits_title": "¿Descartar ediciones?", + "editor_edits_applied_error": "Fallo al aplicar las ediciones", + "editor_edits_applied_success": "Edición aplicada con éxito", "editor_flip_horizontal": "Girar horizontalmente", "editor_flip_vertical": "Girar verticalmente", "editor_orientation": "Orientación", @@ -1002,12 +1014,12 @@ "editor_rotate_left": "Rotar 90º sentido antihorario", "editor_rotate_right": "Rotar 90º sentido horario", "email": "Correo electrónico", - "email_notifications": "Notificaciones por correo electrónico", + "email_notifications": "Notificaciones por correo", "empty_folder": "Esta carpeta está vacía", "empty_trash": "Vaciar papelera", - "empty_trash_confirmation": "¿Estás seguro de que quieres vaciar la papelera? Esto eliminará permanentemente todos los archivos de la basura de Immich.\n¡No podrás deshacer esta acción!", + "empty_trash_confirmation": "¿Estás seguro de que quieres vaciar la papelera? Esto eliminará permanentemente todos los recursos de la papelera de Immich.\n¡No podrás deshacer esta acción!", "enable": "Habilitar", - "enable_backup": "Habilitar Copia de Seguridad", + "enable_backup": "Habilitar copia de seguridad", "enable_biometric_auth_description": "Introduce tu código PIN para habilitar la autentificación biométrica", "enabled": "Habilitado", "end_date": "Fecha final", @@ -1017,48 +1029,48 @@ "enter_your_pin_code_subtitle": "Introduce tu código PIN para acceder a la carpeta protegida", "error": "Error", "error_change_sort_album": "No se pudo cambiar el orden de visualización del álbum", - "error_delete_face": "Error al eliminar la cara del archivo", + "error_delete_face": "Error al eliminar la cara del recurso", "error_getting_places": "Error obteniendo lugares", "error_loading_albums": "Error al cargar álbumes", "error_loading_image": "Error al cargar la imagen", "error_loading_partners": "Error al cargar miembros: {error}", - "error_retrieving_asset_information": "Error al recuperar la información del activo", + "error_retrieving_asset_information": "Error al recuperar la información del recurso", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error al etiquetar la cara: no se pueden obtener las coordenadas del marco", "error_title": "Error: algo salió mal", - "error_while_navigating": "Error al navegar al activo", + "error_while_navigating": "Error al navegar al recurso", "errors": { - "cannot_navigate_next_asset": "No puedes navegar al siguiente archivo", - "cannot_navigate_previous_asset": "No puedes navegar al archivo anterior", + "cannot_navigate_next_asset": "No puedes navegar al siguiente recurso", + "cannot_navigate_previous_asset": "No puedes navegar al recurso anterior", "cant_apply_changes": "No se pueden aplicar los cambios", "cant_change_activity": "No se puede realizar la actividad {enabled, select, true {disable} other {enable}}", - "cant_change_asset_favorite": "No se puede cambiar favorito para este archivo", - "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# elemento} other {# elementos}}", + "cant_change_asset_favorite": "No se puede cambiar favorito para este recurso", + "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos {count, plural, one {# del recurso} other {# de los recursos}}", "cant_get_faces": "No se encuentran caras", "cant_get_number_of_comments": "No se puede obtener la cantidad de comentarios", "cant_search_people": "No se puede buscar a personas", "cant_search_places": "No se pueden buscar lugares", - "error_adding_assets_to_album": "Error al añadir los elementos al álbum", + "error_adding_assets_to_album": "Error al añadir los recursos al álbum", "error_adding_users_to_album": "Error al añadir los usuarios al álbum", "error_deleting_shared_user": "Error al eliminar usuario compartido", "error_downloading": "Error al descargar {filename}", "error_hiding_buy_button": "Error al ocultar el botón de compra", - "error_removing_assets_from_album": "Error al eliminar archivos del álbum; consulte la consola para obtener más detalles", - "error_selecting_all_assets": "Error al seleccionar todos los archivos", + "error_removing_assets_from_album": "Error al eliminar recursos del álbum; consulte la consola para obtener más detalles", + "error_selecting_all_assets": "Error al seleccionar todos los recursos", "exclusion_pattern_already_exists": "Este patrón de exclusión ya existe.", "failed_to_create_album": "Error al crear el álbum", "failed_to_create_shared_link": "Error al crear el enlace compartido", "failed_to_edit_shared_link": "Error al editar el enlace compartido", "failed_to_get_people": "No se logró conseguir gente", - "failed_to_keep_this_delete_others": "No se pudo conservar este activo y eliminar los demás", - "failed_to_load_asset": "Error al cargar el elemento", - "failed_to_load_assets": "Error al cargar los elementos", + "failed_to_keep_this_delete_others": "No se pudo conservar este recurso y eliminar los demás", + "failed_to_load_asset": "Error al cargar el recurso", + "failed_to_load_assets": "Error al cargar los recursos", "failed_to_load_notifications": "Error al cargar las notificaciones", "failed_to_load_people": "Error al cargar a los usuarios", "failed_to_remove_product_key": "No se pudo eliminar la clave del producto", "failed_to_reset_pin_code": "No se pudo restablecer el código PIN", - "failed_to_stack_assets": "No se pudieron agrupar los archivos", - "failed_to_unstack_assets": "Error al desagrupar los archivos", + "failed_to_stack_assets": "No se pudieron agrupar los recursos", + "failed_to_unstack_assets": "Error al desagrupar los recursos", "failed_to_update_notification_status": "Error al actualizar el estado de la notificación", "incorrect_email_or_password": "Contraseña o email incorrecto", "library_folder_already_exists": "Esta ruta de importación ya existe.", @@ -1067,17 +1079,17 @@ "quota_higher_than_disk_size": "Se ha establecido una cuota superior al tamaño del disco", "something_went_wrong": "Algo salió mal", "unable_to_add_album_users": "No se pueden añadir usuarios al álbum", - "unable_to_add_assets_to_shared_link": "No se pueden añadir archivos al enlace compartido", + "unable_to_add_assets_to_shared_link": "No se pueden añadir recursos al enlace compartido", "unable_to_add_comment": "No se puede añadir comentario", "unable_to_add_exclusion_pattern": "No se puede añadir el patrón de exclusión", "unable_to_add_partners": "No se pueden añadir miembros", - "unable_to_add_remove_archive": "No se puede archivar {archived, select, true {remove asset from} other {add asset to}}", - "unable_to_add_remove_favorites": "No se pudo {favorite, select, true {añadir el elemento a} other {eliminar el elemento de}} los favoritos", + "unable_to_add_remove_archive": "No se pudo {archived, select, true {eliminar el recurso del} other {añadir el recurso al}} archivo", + "unable_to_add_remove_favorites": "No se pudo {favorite, select, true {añadir el recuso a} other {eliminar el recurso de}} los favoritos", "unable_to_archive_unarchive": "No se pudo {archived, select, true {agregar el elemento al} other {quitar el elemento del}} archivo", "unable_to_change_album_user_role": "No se puede cambiar la función del usuario del álbum", "unable_to_change_date": "No se puede cambiar la fecha", "unable_to_change_description": "Imposible cambiar la descripción", - "unable_to_change_favorite": "Imposible cambiar el archivo favorito", + "unable_to_change_favorite": "Imposible cambiar el recurso favorito", "unable_to_change_location": "No se puede cambiar de ubicación", "unable_to_change_password": "No se puede cambiar la contraseña", "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# persona} other {# personas}}", @@ -1090,8 +1102,8 @@ "unable_to_create_library": "No se puede crear la biblioteca", "unable_to_create_user": "No se puede crear usuario", "unable_to_delete_album": "No se puede eliminar el álbum", - "unable_to_delete_asset": "No se puede eliminar el archivo", - "unable_to_delete_assets": "Error al eliminar archivos", + "unable_to_delete_asset": "No se puede eliminar el recurso", + "unable_to_delete_assets": "Error al eliminar recursos", "unable_to_delete_exclusion_pattern": "No se puede eliminar el patrón de exclusión", "unable_to_delete_shared_link": "No se puede eliminar el enlace compartido", "unable_to_delete_user": "No se puede eliminar el usuario", @@ -1110,19 +1122,19 @@ "unable_to_log_out_device": "No se puede cerrar la sesión en el dispositivo", "unable_to_login_with_oauth": "No se puede iniciar sesión con OAuth", "unable_to_play_video": "No se puede reproducir el vídeo", - "unable_to_reassign_assets_existing_person": "No se pueden reasignar a {name, select, null {an existing person} other {{name}}}", - "unable_to_reassign_assets_new_person": "No se pueden reasignar archivos a una nueva persona", + "unable_to_reassign_assets_existing_person": "No se pueden reasignar los recursos a {name, select, null {una persona existente} other {{name}}}", + "unable_to_reassign_assets_new_person": "No se pueden reasignar recursos a una nueva persona", "unable_to_refresh_user": "No se puede actualizar el usuario", "unable_to_remove_album_users": "No se pueden eliminar usuarios del álbum", "unable_to_remove_api_key": "No se puede eliminar la clave API", - "unable_to_remove_assets_from_shared_link": "No se pueden eliminar archivos desde el enlace compartido", + "unable_to_remove_assets_from_shared_link": "No se pueden eliminar recursos desde el enlace compartido", "unable_to_remove_library": "No se puede eliminar la biblioteca", "unable_to_remove_partner": "No se puede eliminar el invitado", "unable_to_remove_reaction": "No se puede eliminar la reacción", "unable_to_reset_password": "No se puede restablecer la contraseña", "unable_to_reset_pin_code": "No se ha podido restablecer el PIN", "unable_to_resolve_duplicate": "No se resolver duplicado", - "unable_to_restore_assets": "No se pueden restaurar los archivos", + "unable_to_restore_assets": "No se pueden restaurar los recursos", "unable_to_restore_trash": "No se puede restaurar la papelera", "unable_to_restore_user": "No se puede restaurar el usuario", "unable_to_save_album": "No se puede guardar el álbum", @@ -1137,7 +1149,7 @@ "unable_to_set_profile_picture": "No se puede configurar la imagen de perfil", "unable_to_set_rating": "No se ha podido establecer la calificación", "unable_to_submit_job": "No se puede enviar el trabajo", - "unable_to_trash_asset": "No se puede eliminar el archivo", + "unable_to_trash_asset": "No se puede mover a la papelera el recurso", "unable_to_unlink_account": "No se puede desvincular la cuenta", "unable_to_unlink_motion_video": "No se puede desvincular el vídeo en movimiento", "unable_to_update_album_cover": "No se puede actualizar la portada del álbum", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONAS", "exif_bottom_sheet_person_add_person": "Añadir nombre", "exit_slideshow": "Salir de la presentación", + "expand": "Expandir", "expand_all": "Expandir todo", "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", @@ -1184,23 +1197,25 @@ "failed": "Fallido", "failed_count": "Fallido: {count}", "failed_to_authenticate": "Fallo al autentificar", - "failed_to_load_assets": "Error al cargar los activos", + "failed_to_load_assets": "Error al cargar los recursos", "failed_to_load_folder": "No se pudo cargar la carpeta", "favorite": "Favorito", "favorite_action_prompt": "{count} añadido(s) a Favoritos", "favorite_or_unfavorite_photo": "Foto favorita o no favorita", "favorites": "Favoritos", - "favorites_page_no_favorites": "No se encontraron elementos marcados como favoritos", + "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos", "feature_photo_updated": "Foto destacada actualizada", "features": "Características", - "features_in_development": "Funciones en Desarrollo", - "features_setting_description": "Administrar las funciones de la aplicación", + "features_in_development": "Características en desarrollo", + "features_setting_description": "Administrar las características de la aplicación", "file_name_or_extension": "Nombre del archivo o extensión", + "file_name_text": "Nombre del archivo", + "file_name_with_value": "Nombre del archivo: {file_name}", "file_size": "Tamaño del archivo", "filename": "Nombre del archivo", "filetype": "Tipo de archivo", "filter": "Filtro", - "filter_description": "Condiciones para filtrar los activos objetivo", + "filter_description": "Condiciones para filtrar los recursos objetivo", "filter_people": "Filtrar personas", "filter_places": "Filtrar lugares", "filters": "Filtros", @@ -1220,7 +1235,7 @@ "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidad carga recursos externos desde Google para poder funcionar.", "general": "General", - "geolocation_instruction_location": "Da click en un asset con coordenadas GPS para usar su ubicacion, o selecciona una ubicacion directamente en el mapa", + "geolocation_instruction_location": "Clica en un recurso con coordenadas GPS para usar su ubicación, o selecciona una ubicación directamente en el mapa", "get_help": "Solicitar ayuda", "get_people_error": "Error al obtener gente", "get_wifiname_error": "No se pudo obtener el nombre de la red Wi-Fi. Asegúrate de haber concedido los permisos necesarios y de estar conectado a una red Wi-Fi", @@ -1240,8 +1255,8 @@ "haptic_feedback_switch": "Activar respuesta háptica", "haptic_feedback_title": "Respuesta háptica", "has_quota": "Cuota asignada", - "hash_asset": "Generar hash del archivo", - "hashed_assets": "Archivos con hash generado", + "hash_asset": "Generar hash del recurso", + "hashed_assets": "Recursos con hash generado", "hashing": "Generando hash", "header_settings_add_header_tip": "Añadir cabecera", "header_settings_field_validator_msg": "El valor no puede estar vacío", @@ -1258,22 +1273,22 @@ "hide_schema": "Ocultar esquema", "hide_text_recognition": "Ocultar reconocimiento de texto", "hide_unnamed_people": "Ocultar personas anónimas", - "home_page_add_to_album_conflicts": "{added} elementos añadidos al álbum {album}.{failed} elementos ya existen en el álbum.", - "home_page_add_to_album_err_local": "Aún no se pueden añadir elementos locales a álbumes, omitiendo", - "home_page_add_to_album_success": "Se añadieron {added} elementos al álbum {album}.", - "home_page_album_err_partner": "Aún no se pueden añadir elementos de un compañero a un álbum , omitiendo", - "home_page_archive_err_local": "Los elementos locales no pueden ser archivados, omitiendo", - "home_page_archive_err_partner": "No se pueden archivar los elementos de un compañero, omitiendo", + "home_page_add_to_album_conflicts": "{added} recursos añadidos al álbum {album}.{failed} recursos ya existen en el álbum.", + "home_page_add_to_album_err_local": "Aún no se pueden añadir recursos locales a álbumes, omitiendo", + "home_page_add_to_album_success": "Se añadieron {added} recursos al álbum {album}.", + "home_page_album_err_partner": "Aún no se pueden añadir recursos de un compañero a un álbum, omitiendo", + "home_page_archive_err_local": "Los recursos locales no pueden ser archivados, omitiendo", + "home_page_archive_err_partner": "No se pueden archivar los recursos de un compañero, omitiendo", "home_page_building_timeline": "Construyendo la línea de tiempo", - "home_page_delete_err_partner": "No se pueden eliminar los elementos de un compañero, omitiendo", - "home_page_delete_remote_err_local": "Elementos locales en la selección de eliminación remota, omitiendo", - "home_page_favorite_err_local": "Aún no se pueden marcar como favoritos los elementos locales, omitiendo", - "home_page_favorite_err_partner": "Aún no se pueden marcar los como favoritos los elementos de un compañero, omitiendo", + "home_page_delete_err_partner": "No se pueden eliminar los recursos de un compañero, omitiendo", + "home_page_delete_remote_err_local": "Recursos locales en la selección de eliminación remota, omitiendo", + "home_page_favorite_err_local": "Aún no se pueden marcar como favoritos los recursos locales, omitiendo", + "home_page_favorite_err_partner": "Aún no se pueden marcar como favoritos los recursos de un compañero, omitiendo", "home_page_first_time_notice": "Si es la primera vez que usas la aplicación, asegúrate de elegir un álbum como copia de seguridad para que la línea de tiempo pueda mostrar fotos y vídeos en él", - "home_page_locked_error_local": "No se pueden mover elementos locales a una carpeta protegida, omitiendo", - "home_page_locked_error_partner": "No se pueden mover los elementos de un compañero a una carpeta protegida; omitiendo", - "home_page_share_err_local": "No se pueden compartir elementos locales a través de un enlace, omitiendo", - "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", + "home_page_locked_error_local": "No se pueden mover recursos locales a una carpeta protegida, omitiendo", + "home_page_locked_error_partner": "No se pueden mover los recursos de un compañero a una carpeta protegida, omitiendo", + "home_page_share_err_local": "No se pueden compartir recursos locales a través de un enlace, omitiendo", + "home_page_upload_err_limit": "Solo se pueden subir 30 recursos simultáneamente, omitiendo", "host": "Host", "hour": "Hora", "hours": "Horas", @@ -1293,11 +1308,11 @@ "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {person3} el {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {additionalCount, number} más el {date}", "image_saved_successfully": "Imágenes guardas", - "image_viewer_page_state_provider_download_started": "Descarga Iniciada", + "image_viewer_page_state_provider_download_started": "Descarga iniciada", "image_viewer_page_state_provider_download_success": "Descarga exitosa", "image_viewer_page_state_provider_share_error": "Error al compartir", "immich_logo": "Logo de Immich", - "immich_web_interface": "Interfaz Web de Immich", + "immich_web_interface": "Interfaz web de Immich", "import_from_json": "Importar desde JSON", "import_path": "Importar ruta", "in_albums": "En {count, plural, one {# álbum} other {# álbumes}}", @@ -1306,7 +1321,7 @@ "in_year_selector": "En", "include_archived": "Incluir archivados", "include_shared_albums": "Incluir álbumes compartidos", - "include_shared_partner_assets": "Incluir elementos compartidos por compañeros", + "include_shared_partner_assets": "Incluir recursos compartidos por compañeros", "individual_share": "Compartir individualmente", "individual_shares": "Acciones individuales", "info": "Información", @@ -1318,7 +1333,7 @@ }, "invalid_date": "Fecha incorrecta", "invalid_date_format": "Formato de fecha incorrecto", - "invite_people": "Invitar a Personas", + "invite_people": "Invitar a personas", "invite_to_album": "Invitar al álbum", "ios_debug_info_fetch_ran_at": "Busca ejecución en {dateTime}", "ios_debug_info_last_sync_at": "Última sincronización en {dateTime}", @@ -1333,21 +1348,21 @@ "keep": "Conservar", "keep_albums": "Conservar álbumes", "keep_albums_count": "Mantener {count} {count, plural, one {álbum} other {álbumes}}", - "keep_all": "Conservar Todo", + "keep_all": "Conservar todo", "keep_description": "Elige qué permanece en tu dispositivo al liberar espacio.", "keep_favorites": "Mantener favoritos", "keep_on_device": "Mantener en el dispositivo", "keep_on_device_hint": "Seleccionar elementos para conservar en este dispositivo", "keep_this_delete_others": "Mantener este, eliminar los otros", "keeping": "Manteniendo: {items}", - "kept_this_deleted_others": "Mantuvo este activo y eliminó {count, plural, one {# activo} other {# activos}}", + "kept_this_deleted_others": "Mantuvo este recurso y eliminó {count, plural, one {# recurso} other {# recursos}}", "keyboard_shortcuts": "Atajos de teclado", "language": "Idioma", "language_no_results_subtitle": "Intente ajustar el término de búsqueda", "language_no_results_title": "No se han encontrado idiomas", "language_search_hint": "Buscar idiomas...", "language_setting_description": "Selecciona tu idioma preferido", - "large_files": "Archivos Grandes", + "large_files": "Archivos grandes", "last": "Último", "last_months": "{count, plural, one {Último mes} other {Últimos # meses}}", "last_seen": "Ultima vez visto", @@ -1364,7 +1379,7 @@ "library_options": "Opciones de biblioteca", "library_page_device_albums": "Álbumes en el dispositivo", "library_page_new_album": "Nuevo álbum", - "library_page_sort_asset_count": "Número de elementos", + "library_page_sort_asset_count": "Número de recursos", "library_page_sort_created": "Creado más recientemente", "library_page_sort_last_modified": "Última modificación", "library_page_sort_title": "Título del álbum", @@ -1380,9 +1395,9 @@ "loading_search_results_failed": "Error al cargar los resultados de la búsqueda", "local": "Local", "local_asset_cast_failed": "No es posible transmitir un recurso que no está subido al servidor", - "local_assets": "Archivos Locales", + "local_assets": "Recursos locales", "local_id": "ID local", - "local_media_summary": "Resumen de Medios Locales", + "local_media_summary": "Resumen de medios locales", "local_network": "Red local", "local_network_sheet_info": "La aplicación se conectará al servidor a través de esta URL cuando utilice la red Wi-Fi especificada", "location": "Ubicación", @@ -1419,7 +1434,7 @@ "login_form_handshake_exception": "Hubo una excepción de handshake con el servidor. Activa la compatibilidad con certificados autofirmados en la configuración si estás utilizando un certificado autofirmado.", "login_form_password_hint": "contraseña", "login_form_save_login": "Mantener la sesión iniciada", - "login_form_server_empty": "Agrega la URL del servidor.", + "login_form_server_empty": "Introduce la URL del servidor.", "login_form_server_error": "No se pudo conectar al servidor.", "login_has_been_disabled": "El inicio de sesión ha sido deshabilitado.", "login_password_changed_error": "Hubo un error actualizando la contraseña", @@ -1474,11 +1489,11 @@ "map_cannot_get_user_location": "No se pudo obtener la posición del usuario", "map_location_dialog_yes": "Sí", "map_location_picker_page_use_location": "Usar esta ubicación", - "map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar elementos de tu ubicación actual. ¿Deseas activarlos ahora?", + "map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar recursos de tu ubicación actual. ¿Deseas activarlos ahora?", "map_location_service_disabled_title": "Servicios de ubicación desactivados", "map_marker_for_images": "Marcador de mapa para imágenes tomadas en {city}, {country}", "map_marker_with_image": "Marcador de mapa con imagen", - "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 recursos de tu ubicación actual. ¿Deseas activarlos ahora?", "map_no_location_permission_title": "Permisos de ubicación denegados", "map_settings": "Ajustes del mapa", "map_settings_dark_mode": "Modo oscuro", @@ -1490,13 +1505,13 @@ "map_settings_include_show_archived": "Incluir archivados", "map_settings_include_show_partners": "Incluir miembros", "map_settings_only_show_favorites": "Mostrar solo favoritas", - "map_settings_theme_settings": "Apariencia del Mapa", + "map_settings_theme_settings": "Tema del mapa", "map_zoom_to_see_photos": "Alejar para ver fotos", "mark_all_as_read": "Marcar todo como leído", "mark_as_read": "Marcar como leído", "marked_all_as_read": "Todos marcados como leídos", "matches": "Coincidencias", - "matching_assets": "Elementos Coincidentes", + "matching_assets": "Recursos coincidentes", "media_type": "Tipo de medio", "memories": "Recuerdos", "memories_all_caught_up": "Puesto al día", @@ -1519,7 +1534,7 @@ "mirror_horizontal": "Horizontal", "mirror_vertical": "Vertical", "missing": "Faltante", - "mobile_app": "Aplicación Móvil", + "mobile_app": "Aplicación móvil", "mobile_app_download_onboarding_note": "Descarga la aplicación móvil utilizando las siguientes opciones", "model": "Modelo", "month": "Mes", @@ -1537,15 +1552,15 @@ "moved_to_archive": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a archivo", "moved_to_library": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a biblioteca", "moved_to_trash": "Movido a la papelera", - "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo", - "multiselect_grid_edit_gps_err_read_only": "No se puede editar la ubicación de activos de solo lectura, omitiendo", - "mute_memories": "Silenciar Recuerdos", + "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del recurso(s) de solo lectura, omitiendo", + "multiselect_grid_edit_gps_err_read_only": "No se puede editar la ubicación de recursos de solo lectura, omitiendo", + "mute_memories": "Silenciar recuerdos", "my_albums": "Mis álbumes", "name": "Nombre", "name_or_nickname": "Nombre o apodo", "name_required": "El nombre es obligatorio", "navigate": "Navegar", - "navigate_to_time": "Navegar a Hora", + "navigate_to_time": "Navegar a la hora", "network_requirement_photos_upload": "Usar datos móviles para crear una copia de seguridad de las fotos", "network_requirement_videos_upload": "Usar datos móviles para crear una copia de seguridad de los videos", "network_requirements": "Requisitos de red", @@ -1560,7 +1575,7 @@ "new_person": "Nueva persona", "new_pin_code": "Nuevo PIN", "new_pin_code_subtitle": "Esta es la primera vez que accedes a la carpeta protegida. Crea un código PIN seguro para acceder a esta página", - "new_timeline": "Nueva Línea de tiempo", + "new_timeline": "Nueva línea de tiempo", "new_update": "Nueva actualización", "new_user_created": "Nuevo usuario creado", "new_version_available": "NUEVA VERSIÓN DISPONIBLE", @@ -1575,10 +1590,10 @@ "no_albums_yet": "Parece que aún no tienes ningún álbum.", "no_archived_assets_message": "Archive fotos y videos para ocultarlos de su vista de Fotos", "no_assets_message": "Haz clic para subir tu primera foto", - "no_assets_to_show": "No hay elementos a mostrar", + "no_assets_to_show": "No hay recursos a mostrar", "no_cast_devices_found": "No se encontraron dispositivos de transmisión", - "no_checksum_local": "Suma de verificación no disponible. No se pueden obtener los elementos locales", - "no_checksum_remote": "Suma de verificación no disponible. No se puede obtener el elemento remoto", + "no_checksum_local": "Suma de verificación no disponible. No se pueden obtener los recursos locales", + "no_checksum_remote": "Suma de verificación no disponible. No se puede obtener el recurso remoto", "no_configuration_needed": "No se necesita configuración", "no_devices": "Dispositivos no autorizados", "no_duplicates_found": "No se encontraron duplicados.", @@ -1587,14 +1602,14 @@ "no_favorites_message": "Añade favoritos para encontrar rápidamente sus mejores fotos y videos", "no_filters_added": "Aún no se han añadido filtros", "no_libraries_message": "Crea una biblioteca externa para ver tus fotos y vídeos", - "no_local_assets_found": "No se encontraron elementos locales con esta suma de comprobación", + "no_local_assets_found": "No se encontraron recursos locales con esta suma de comprobación", "no_location_set": "No se ha establecido ninguna ubicación", "no_locked_photos_message": "Las fotos y los vídeos de la carpeta protegida se mantienen ocultos; no aparecerán cuando veas o busques elementos en tu biblioteca.", "no_name": "Sin nombre", "no_notifications": "Ninguna notificación", "no_people_found": "No se encontraron personas coincidentes", "no_places": "Sin lugares", - "no_remote_assets_found": "No se encontraron elementos remotos con esta suma de comprobación", + "no_remote_assets_found": "No se encontraron recursos remotos con esta suma de comprobación", "no_results": "Sin resultados", "no_results_description": "Pruebe con un sinónimo o una palabra clave más general", "no_shared_albums_message": "Crea un álbum para compartir fotos y vídeos con personas de tu red", @@ -1604,13 +1619,12 @@ "not_available": "N/D", "not_in_any_album": "Sin álbum", "not_selected": "No seleccionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos que ya se subieron, ejecute la", "notes": "Notas", "nothing_here_yet": "Sin nada aún", "notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.", "notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.", "notification_permission_list_tile_enable_button": "Permitir notificaciones", - "notification_permission_list_tile_title": "Permisos de Notificacion", + "notification_permission_list_tile_title": "Permiso de notificación", "notification_toggle_setting_description": "Habilitar notificaciones de correo electrónico", "notifications": "Notificaciones", "notifications_setting_description": "Administrar notificaciones", @@ -1634,6 +1648,7 @@ "online": "En línea", "only_favorites": "Solo favoritos", "open": "Abierto", + "open_calendar": "Abrir calendario", "open_in_map_view": "Abrir en la vista del mapa", "open_in_openstreetmap": "Abrir en OpenStreetMap", "open_the_search_filters": "Abre los filtros de búsqueda", @@ -1652,7 +1667,7 @@ "page": "Página", "partner": "Compañero", "partner_can_access": "{partner} tiene acceso", - "partner_can_access_assets": "Todas tus fotos y vídeos excepto los Archivados y Eliminados", + "partner_can_access_assets": "Todas tus fotos y vídeos excepto los archivados y eliminados", "partner_can_access_location": "Ubicación donde fueron realizadas tus fotos", "partner_list_user_photos": "Fotos de {user}", "partner_list_view_all": "Ver todas", @@ -1683,14 +1698,14 @@ "people_edits_count": "Editada {count, plural, one {# persona} other {# personas}}", "people_feature_description": "Explorar fotos y vídeos agrupados por personas", "people_selected": "{count, plural, one {# persona seleccionada} other {# personas seleccionadas}}", - "people_sidebar_description": "Mostrar un enlace a Personas en la barra lateral", + "people_sidebar_description": "Mostrar un enlace a personas en la barra lateral", "permanent_deletion_warning": "Advertencia de eliminación permanente", - "permanent_deletion_warning_setting_description": "Mostrar una advertencia al eliminar archivos permanentemente", + "permanent_deletion_warning_setting_description": "Mostrar una advertencia al eliminar recursos permanentemente", "permanently_delete": "Borrar permanentemente", - "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {elemento} other {elementos}}", - "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {este activo?} other {estos # activos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).", - "permanently_deleted_asset": "Archivo eliminado permanentemente", - "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", + "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {recurso} other {recursos}}", + "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {este recurso?} other {estos # recursos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).", + "permanently_deleted_asset": "Recurso eliminado permanentemente", + "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# recurso} other {# recursos}}", "permission": "Permiso", "permission_empty": "Tus permisos no deben estar vacíos", "permission_onboarding_back": "Volver", @@ -1711,7 +1726,7 @@ "person_selected": "Persona seleccionada", "photo_shared_all_users": "Parece que compartiste tus fotos con todos los usuarios o no tienes ningún usuario con quien compartirlas.", "photos": "Fotos", - "photos_and_videos": "Fotos y Vídeos", + "photos_and_videos": "Fotos y vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de años anteriores", "photos_only": "Solo fotos", @@ -1749,14 +1764,14 @@ "privacy": "Privacidad", "profile": "Perfil", "profile_drawer_app_logs": "Registros", - "profile_drawer_client_server_up_to_date": "Cliente y Servidor están actualizados", + "profile_drawer_client_server_up_to_date": "Cliente y servidor están actualizados", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Modo Solo lectura habilitado. Mantén pulsado el icono del avatar del usuario para salir.", + "profile_drawer_readonly_mode": "Modo solo lectura habilitado. Mantén pulsado el icono del avatar del usuario para salir.", "profile_image_of_user": "Foto de perfil de {user}", "profile_picture_set": "Conjunto de imágenes de perfil.", "public_album": "Álbum público", "public_share": "Compartir públicamente", - "purchase_account_info": "Seguidor", + "purchase_account_info": "Colaborador", "purchase_activated_subtitle": "Gracias por apoyar a Immich y al software de código abierto", "purchase_activated_time": "Activado el {date}", "purchase_activated_title": "Su clave ha sido activada correctamente", @@ -1769,7 +1784,7 @@ "purchase_button_select": "Seleccionar", "purchase_failed_activation": "¡Error al activar! ¡Por favor, revisa tu correo electrónico para obtener la clave del producto correcta!", "purchase_individual_description_1": "Para un usuario", - "purchase_individual_description_2": "Estado de soporte", + "purchase_individual_description_2": "Estatus de colaborador", "purchase_individual_title": "Individual", "purchase_input_suggestion": "¿Tiene una clave de producto? Introdúzcala a continuación", "purchase_license_subtitle": "Compre Immich para apoyar el desarrollo continuo del servicio", @@ -1785,12 +1800,12 @@ "purchase_remove_server_product_key": "Eliminar la clave de producto del servidor", "purchase_remove_server_product_key_prompt": "¿Está seguro de que desea eliminar la clave de producto del servidor?", "purchase_server_description_1": "Para todo el servidor", - "purchase_server_description_2": "Estado del soporte", + "purchase_server_description_2": "Estatus de colaborador", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador", - "query_asset_id": "Consultar ID de elemento", + "query_asset_id": "Consultar ID de recurso", "queue_status": "Poniendo en cola {count}/{total}", - "rate_asset": "Valorar activo", + "rate_asset": "Valorar recurso", "rating": "Valoración", "rating_clear": "Borrar calificación", "rating_count": "{count, plural, one {# estrella} other {# estrellas}}", @@ -1798,20 +1813,20 @@ "rating_set": "Calificación establecida en {rating, plural, one {# estrella} other {# estrellas}}", "reaction_options": "Opciones de reacción", "read_changelog": "Leer registro de cambios", - "readonly_mode_disabled": "Modo Solo lectura deshabilitado", - "readonly_mode_enabled": "Modo Solo lectura habilitado", + "readonly_mode_disabled": "Modo solo lectura deshabilitado", + "readonly_mode_enabled": "Modo solo lectura habilitado", "ready_for_upload": "Listo para subir", "reassign": "Reasignar", - "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a {name, select, null {una persona existente} other {{name}}}", - "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a un nuevo usuario", - "reassing_hint": "Asignar archivos seleccionados a una persona existente", + "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# recurso} other {# recursos}} a {name, select, null {una persona existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# recurso} other {# recursos}} a un nuevo usuario", + "reassing_hint": "Asignar recursos seleccionados a una persona existente", "recent": "Reciente", - "recent-albums": "Últimos álbumes", + "recent_albums": "Últimos álbumes", "recent_searches": "Búsquedas recientes", "recently_added": "Añadidos recientemente", "recently_added_page_title": "Recién añadidos", "recently_taken": "Tomadas recientemente", - "recently_taken_page_title": "Tomadas Recientemente", + "recently_taken_page_title": "Tomadas recientemente", "refresh": "Actualizar", "refresh_encoded_videos": "Recargar los vídeos codificados", "refresh_faces": "Actualizar caras", @@ -1824,14 +1839,14 @@ "refreshing_metadata": "Recargando metadatos", "regenerating_thumbnails": "Recargando miniaturas", "remote": "Remoto", - "remote_assets": "Elementos remotos", - "remote_media_summary": "Resumen de Medios Remotos", + "remote_assets": "Recursos remotos", + "remote_media_summary": "Resumen de medios remotos", "remove": "Eliminar", - "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del álbum?", - "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del enlace compartido?", - "remove_assets_title": "¿Eliminar activos?", + "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# recurso} other {# recursos}} del álbum?", + "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# recurso} other {# recursos}} del enlace compartido?", + "remove_assets_title": "¿Eliminar recursos?", "remove_custom_date_range": "Eliminar intervalo de fechas personalizado", - "remove_deleted_assets": "Eliminar archivos sin conexión", + "remove_deleted_assets": "Eliminar recursos sin conexión", "remove_from_album": "Eliminar del álbum", "remove_from_album_action_prompt": "{count} eliminado del álbum", "remove_from_favorites": "Quitar de favoritos", @@ -1850,7 +1865,7 @@ "removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos", "removed_memory": "Recuerdo eliminado", "removed_photo_from_memory": "Foto eliminada del recuerdo", - "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}", + "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# recurso} other {# recursos}}", "rename": "Renombrar", "repair": "Reparar", "repair_no_results_message": "Los archivos perdidos y sin seguimiento aparecerán aquí", @@ -1877,7 +1892,7 @@ "restore_all": "Restaurar todo", "restore_trash_action_prompt": "{count} restaurado de la papelera", "restore_user": "Restaurar usuario", - "restored_asset": "Archivo restaurado", + "restored_asset": "Recurso restaurado", "resume": "Continuar", "resume_paused_jobs": "Reanudar {count, plural, one {# tarea en pausa} other {# tareas en pausa}}", "retry_upload": "Reintentar subida", @@ -1938,7 +1953,7 @@ "search_no_result": "No se encontraron resultados, prueba con un término o combinación de búsqueda diferente", "search_options": "Opciones de búsqueda", "search_page_categories": "Categorías", - "search_page_motion_photos": "Foto en Movimiento", + "search_page_motion_photos": "Fotos en movimiento", "search_page_no_objects": "No hay información de objetos disponibles", "search_page_no_places": "No hay información de lugares disponibles", "search_page_screenshots": "Capturas de pantalla", @@ -1947,7 +1962,7 @@ "search_page_things": "Cosas", "search_page_view_all_button": "Ver todo", "search_page_your_activity": "Tu actividad", - "search_page_your_map": "Tu Mapa", + "search_page_your_map": "Tu mapa", "search_people": "Buscar personas", "search_places": "Buscar lugar", "search_rating": "Buscar por calificación...", @@ -1982,7 +1997,7 @@ "select_people": "Seleccionar gente", "select_person": "Seleccionar persona", "select_person_to_tag": "Elija una persona a etiquetar", - "select_photos": "Seleccionar Fotos", + "select_photos": "Seleccionar fotos", "select_trash_all": "Seleccionar eliminar todo", "select_user_for_sharing_page_err_album": "Fallo al crear el álbum", "selected": "Seleccionado", @@ -1995,7 +2010,7 @@ "server_info_box_server_url": "Enlace del servidor", "server_offline": "Servidor desconectado", "server_online": "Servidor en línea", - "server_privacy": "Privacidad del Servidor", + "server_privacy": "Privacidad del servidor", "server_restarting_description": "Esta página se actualizará en breve.", "server_restarting_title": "El servidor se está reiniciando", "server_stats": "Estadísticas del servidor", @@ -2023,10 +2038,10 @@ "setting_notifications_notify_minutes": "{count} minutos", "setting_notifications_notify_never": "nunca", "setting_notifications_notify_seconds": "{count} segundos", - "setting_notifications_single_progress_subtitle": "Información detallada del progreso de subida de cada archivo", + "setting_notifications_single_progress_subtitle": "Información detallada del progreso de subida de cada recurso", "setting_notifications_single_progress_title": "Mostrar progreso detallado de copia de seguridad en segundo plano", "setting_notifications_subtitle": "Ajusta tus preferencias de notificación", - "setting_notifications_total_progress_subtitle": "Progreso general de subida (elementos completados/total)", + "setting_notifications_total_progress_subtitle": "Progreso general de subida (recursos completados/total)", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_video_viewer_auto_play_subtitle": "Reproducir vídeos automáticamente al abrirlos", "setting_video_viewer_auto_play_title": "Reproducir vídeos automáticamente", @@ -2042,12 +2057,12 @@ "share_add_photos": "Añadir fotos", "share_assets_selected": "{count} seleccionado(s)", "share_dialog_preparing": "Preparando...", - "share_link": "Compartir Enlace", + "share_link": "Compartir enlace", "shared": "Compartidos", "shared_album_activities_input_disable": "Los comentarios están deshabilitados", "shared_album_activity_remove_content": "¿Deseas eliminar esta actividad?", - "shared_album_activity_remove_title": "Eliminar Actividad", - "shared_album_section_people_action_error": "Error retirando/eliminando del album", + "shared_album_activity_remove_title": "Eliminar actividad", + "shared_album_section_people_action_error": "Error retirando/eliminando del álbum", "shared_album_section_people_action_leave": "Eliminar usuario del álbum", "shared_album_section_people_action_remove_user": "Eliminar usuario del álbum", "shared_album_section_people_title": "PERSONAS", @@ -2055,7 +2070,7 @@ "shared_by_user": "Compartido por {user}", "shared_by_you": "Compartido por ti", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "{current} / {total} Cargado(s)", + "shared_intent_upload_button_progress_text": "{current} / {total} cargado(s)", "shared_link_app_bar_title": "Enlaces compartidos", "shared_link_clipboard_copied_massage": "Copiado al portapapeles", "shared_link_clipboard_text": "Enlace: {link}\nContraseña: {password}", @@ -2089,7 +2104,7 @@ "shared_link_password_description": "Requerir una contraseña para acceder a este enlace compartido", "shared_links": "Enlaces compartidos", "shared_links_description": "Comparte fotos y vídeos con un enlace", - "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos y vídeos compartidos.}}", + "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos y vídeos compartidos.}}", "shared_with_me": "Compartidos conmigo", "shared_with_partner": "Compartido con {partner}", "sharing": "Compartidos", @@ -2100,7 +2115,7 @@ "sharing_sidebar_description": "Muestra un enlace a \"Compartido\" en el menú lateral", "sharing_silver_appbar_create_shared_album": "Crear un álbum compartido", "sharing_silver_appbar_share_partner": "Compartir con compañero", - "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el archivo", + "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el recurso", "show_album_options": "Mostrar opciones del álbum", "show_albums": "Mostrar álbumes", "show_all_people": "Mostrar todas las personas", @@ -2152,7 +2167,7 @@ "stack_duplicates": "Apilar duplicados", "stack_select_one_photo": "Selecciona una imagen principal para la pila", "stack_selected_photos": "Apilar fotos seleccionadas", - "stacked_assets_count": "Apilado(s) {count, plural, one {# activo} other {# activos}}", + "stacked_assets_count": "Apilado(s) {count, plural, one {# recurso} other {# recursos}}", "stacktrace": "Seguimiento de pila", "start": "Inicio", "start_date": "Fecha de inicio", @@ -2175,6 +2190,7 @@ "support": "Soporte", "support_and_feedback": "Soporte y comentarios", "support_third_party_description": "Esta instalación de Immich fue empaquetada por un tercero. Los problemas actuales pueden ser ocasionados por ese paquete; por favor, discuta sus inconvenientes con el empaquetador antes de usar los enlaces de abajo.", + "supporter": "Colaborador", "swap_merge_direction": "Alternar dirección de mezcla", "sync": "Sincronizar", "sync_albums": "Sincronizar álbumes", @@ -2185,13 +2201,13 @@ "sync_status_subtitle": "Ver y gestionar el estado de la sincronización", "sync_upload_album_setting_subtitle": "Crea y sube tus fotos y videos a los álbumes seleccionados en Immich", "tag": "Etiqueta", - "tag_assets": "Etiquetar activos", + "tag_assets": "Etiquetar recursos", "tag_created": "Etiqueta creada: {tag}", "tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lógicas", "tag_not_found_question": "¿No encuentra una etiqueta? Crea una nueva etiqueta.", "tag_people": "Etiquetar personas", "tag_updated": "Etiqueta actualizada: {tag}", - "tagged_assets": "Etiquetado(s) {count, plural, one {# activo} other {# activos}}", + "tagged_assets": "Etiquetado(s) {count, plural, one {# recurso} other {# recursos}}", "tags": "Etiquetas", "tap_to_run_job": "Toca para ejecutar la tarea", "template": "Plantilla", @@ -2199,8 +2215,8 @@ "theme": "Tema", "theme_selection": "Selección de tema", "theme_selection_description": "Establece el tema automáticamente como \"claro\" u \"oscuro\" según las preferencias del sistema/navegador", - "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", - "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({count})", + "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los recursos", + "theme_setting_asset_list_tiles_per_row_title": "Número de recursos por fila ({count})", "theme_setting_colorful_interface_subtitle": "Aplicar el color primario a las superficies de fondo.", "theme_setting_colorful_interface_title": "Color de Interfaz", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", @@ -2223,7 +2239,7 @@ "to_archive": "Archivar", "to_change_password": "Cambiar contraseña", "to_favorite": "A los favoritos", - "to_login": "Iniciar Sesión", + "to_login": "Iniciar sesión", "to_multi_select": "para multi selección", "to_parent": "Ir a los padres", "to_select": "para seleccionar", @@ -2236,20 +2252,20 @@ "trash_action_prompt": "{count} movidos a la papelera", "trash_all": "Descartar todo", "trash_count": "Descartar {count, number}", - "trash_delete_asset": "Borrar/Eliminar archivo", + "trash_delete_asset": "Borrar/Eliminar recurso", "trash_emptied": "Papelera vaciada", "trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerán aquí.", "trash_page_delete_all": "Eliminar todos", - "trash_page_empty_trash_dialog_content": "¿Está seguro que quiere eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente", + "trash_page_empty_trash_dialog_content": "¿Está seguro que quiere eliminar los recursos? Estos recursos serán eliminados de Immich permanentemente", "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente de forma permanente después de {days} días", - "trash_page_no_assets": "No hay elementos en la papelera", + "trash_page_no_assets": "No hay recursos en la papelera", "trash_page_restore_all": "Restaurar todos", - "trash_page_select_assets_btn": "Seleccionar elementos", + "trash_page_select_assets_btn": "Seleccionar recursos", "trash_page_title": "Papelera ({count})", "trashed_items_will_be_permanently_deleted_after": "Los elementos en la papelera serán eliminados permanentemente tras {days, plural, one {# día} other {# días}}.", "trigger": "Disparador", - "trigger_asset_uploaded": "Activo subido", - "trigger_asset_uploaded_description": "Se activa cuando se carga un nuevo activo", + "trigger_asset_uploaded": "Recurso subido", + "trigger_asset_uploaded_description": "Se activa cuando se carga un nuevo recurso", "trigger_description": "Un evento que inicia el flujo de trabajo", "trigger_person_recognized": "Persona reconocida", "trigger_person_recognized_description": "Se activa cuando se detecta una persona", @@ -2275,7 +2291,7 @@ "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Cuenta OAuth desconectada", "unmute_memories": "Habilitar sonido recuerdos", - "unnamed_album": "Album sin nombre", + "unnamed_album": "Álbum sin nombre", "unnamed_album_delete_confirmation": "¿Seguro que quieres borrar este álbum?", "unnamed_share": "Compartido sin nombre", "unsaved_change": "Cambio no guardado", @@ -2284,24 +2300,24 @@ "unselect_all_in": "Deselecciona todos en {group}", "unstack": "Desapilar", "unstack_action_prompt": "{count} desapilado(s)", - "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}", + "unstacked_assets_count": "Desapilado(s) {count, plural, one {# recurso} other {# recursos}}", "unsupported_field_type": "Tipo de campo no soportado", "untagged": "Sin etiqueta", "untitled_workflow": "Flujo de trabajo sin título", "up_next": "A continuación", - "update_location_action_prompt": "Actualiza la ubicación de {count} assets seleccionados con:", + "update_location_action_prompt": "Actualiza la ubicación de {count} recursos seleccionados con:", "updated_at": "Actualizado", "updated_password": "Contraseña actualizada", "upload": "Subir", "upload_concurrency": "Subidas simultáneas", - "upload_details": "Cargar Detalles", - "upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", - "upload_dialog_title": "Subir elementos", - "upload_error_with_count": "Error al cargar {count, plural, one {# asset} other {# assets}}", + "upload_details": "Cargar detalles", + "upload_dialog_info": "¿Quieres hacer una copia de seguridad al servidor de los recursos seleccionados?", + "upload_dialog_title": "Subir recursos", + "upload_error_with_count": "Error al cargar {count, plural, one {# el recurso} other {# los recursos}}", "upload_errors": "Subida completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de la subida.", "upload_finished": "Carga finalizada", "upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}", - "upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}", + "upload_skipped_duplicates": "Saltado {count, plural, one {# recurso duplicado} other {# recursos duplicados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Errores", "upload_status_uploaded": "Subido", @@ -2317,10 +2333,10 @@ "user": "Usuario", "user_has_been_deleted": "Este usuario ha sido eliminado.", "user_id": "Id. de usuario", - "user_liked": "{user} le gustó {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_liked": "{user} le gustó {type, select, photo {esta foto} video {este vídeo} asset {este recurso} other {esto}}", "user_pin_code_settings": "PIN", "user_pin_code_settings_description": "Gestione su PIN", - "user_privacy": "Privacidad del Usuario", + "user_privacy": "Privacidad del usuario", "user_purchase_settings": "Compra", "user_purchase_settings_description": "Gestiona tu compra", "user_role_set": "Establecer {user} como {role}", @@ -2347,23 +2363,23 @@ "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", "videos_only": "Solo vídeos", "view": "Ver", - "view_album": "Ver Álbum", + "view_album": "Ver álbum", "view_all": "Ver todas", "view_all_users": "Mostrar todos los usuarios", - "view_asset_owners": "Ver propietarios", - "view_details": "Ver Detalles", + "view_asset_owners": "Ver propietarios del recurso", + "view_details": "Ver detalles", "view_in_timeline": "Ver en la línea de tiempo", "view_link": "Ver enlace", "view_links": "Mostrar enlaces", "view_name": "Ver", - "view_next_asset": "Mostrar siguiente elemento", - "view_previous_asset": "Mostrar elemento anterior", + "view_next_asset": "Mostrar siguiente recurso", + "view_previous_asset": "Mostrar recurso anterior", "view_qr_code": "Ver código QR", "view_similar_photos": "Ver fotografías similares", - "view_stack": "Ver Pila", - "view_user": "Ver Usuario", + "view_stack": "Ver pila", + "view_user": "Ver usuario", "viewer_remove_from_stack": "Quitar de la pila", - "viewer_stack_use_as_main_asset": "Usar como elemento principal", + "viewer_stack_use_as_main_asset": "Usar como recurso principal", "viewer_unstack": "Desapilar", "visibility_changed": "Visibilidad cambiada para {count, plural, one {# persona} other {# personas}}", "visual": "Visual", @@ -2372,10 +2388,10 @@ "waiting_count": "Esperando: {count}", "warning": "Advertencia", "week": "Semana", - "welcome": "Bienvenido", - "welcome_to_immich": "Bienvenido a Immich", + "welcome": "Bienvenido/a", + "welcome_to_immich": "Bienvenido/a a Immich", "width": "Ancho", - "wifi_name": "Nombre Wi-Fi", + "wifi_name": "Nombre del Wi-Fi", "workflow_delete_prompt": "¿Estás seguro de que quieres eliminar este flujo de trabajo?", "workflow_deleted": "Flujo de trabajo eliminado", "workflow_description": "Descripción del flujo de trabajo", @@ -2388,14 +2404,14 @@ "workflow_update_success": "Flujo de trabajo actualizado con éxito", "workflow_updated": "Flujo de trabajo actualizado", "workflows": "Flujos de trabajo", - "workflows_help_text": "Los flujos de trabajo automatizan acciones en sus activos según activadores y filtros", + "workflows_help_text": "Los flujos de trabajo automatizan acciones en sus recursos según activadores y filtros", "wrong_pin_code": "Código PIN incorrecto", "year": "Año", "years_ago": "Hace {years, plural, one {# año} other {# años}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tienes ningún enlace compartido", "your_wifi_name": "El nombre de tu Wi-Fi", - "zero_to_clear_rating": "presione 0 para borrar la calificación del activo", - "zoom_image": "Acercar Imagen", + "zero_to_clear_rating": "presione 0 para borrar la calificación del recurso", + "zoom_image": "Acercar imagen", "zoom_to_bounds": "Ajustar a los límites" } diff --git a/i18n/et.json b/i18n/et.json index 61d4e5b949..7b85dcfda6 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -311,7 +311,7 @@ "search_jobs": "Otsi töödet…", "send_welcome_email": "Saada tervituskiri", "server_external_domain_settings": "Väline domeen", - "server_external_domain_settings_description": "Domeen avalikult jagatud linkide jaoks, k.a. http(s)://", + "server_external_domain_settings_description": "Domeen väliste linkide jaoks", "server_public_users": "Avalikud kasutajad", "server_public_users_description": "Kasutaja jagatud albumisse lisamisel kuvatakse kõiki kasutajaid (nime ja e-posti aadressiga). Kui keelatud, kuvatakse kasutajate nimekirja ainult administraatoritele.", "server_settings": "Serveri seaded", @@ -794,6 +794,11 @@ "color": "Värv", "color_theme": "Värviteema", "command": "Käsk", + "command_palette_prompt": "Leia kiirelt lehti, tegevusi või käske", + "command_palette_to_close": "sulge", + "command_palette_to_navigate": "sisene", + "command_palette_to_select": "vali", + "command_palette_to_show_all": "näita kõiki", "comment_deleted": "Kommentaar kustutatud", "comment_options": "Kommentaari valikud", "comments_and_likes": "Kommentaarid ja meeldimised", @@ -997,6 +1002,11 @@ "editor_close_without_save_prompt": "Muudatusi ei salvestata", "editor_close_without_save_title": "Sulge redaktor?", "editor_confirm_reset_all_changes": "Kas oled kindel, et soovid kõik muudatused tühistada?", + "editor_discard_edits_confirm": "Tühista muudatused", + "editor_discard_edits_prompt": "Sul on salvestamata muudatusi. Kas oled kindel, et soovid need tühistada?", + "editor_discard_edits_title": "Tühista muudatused?", + "editor_edits_applied_error": "Muudatuste rakendamine ebaõnnestus", + "editor_edits_applied_success": "Muudatused edukalt rakendatud", "editor_flip_horizontal": "Peegelda horisontaalselt", "editor_flip_vertical": "Peegelda vertikaalselt", "editor_orientation": "Orientatsioon", @@ -1163,6 +1173,7 @@ "exif_bottom_sheet_people": "ISIKUD", "exif_bottom_sheet_person_add_person": "Lisa nimi", "exit_slideshow": "Sulge slaidiesitlus", + "expand": "Laienda", "expand_all": "Näita kõik", "experimental_settings_new_asset_list_subtitle": "Töös", "experimental_settings_new_asset_list_title": "Luba eksperimentaalne fotoruudistik", @@ -1608,7 +1619,6 @@ "not_available": "Pole saadaval", "not_in_any_album": "Pole üheski albumis", "not_selected": "Ei ole valitud", - "note_apply_storage_label_to_previously_uploaded assets": "Märkus: Et rakendada talletussilt varem üleslaaditud üksustele, käivita", "notes": "Märkused", "nothing_here_yet": "Siin pole veel midagi", "notification_permission_dialog_content": "Teavituste lubamiseks mine Seadetesse ja vali lubamine.", @@ -1638,6 +1648,7 @@ "online": "Ühendatud", "only_favorites": "Ainult lemmikud", "open": "Ava", + "open_calendar": "Ava kalender", "open_in_map_view": "Ava kaardi vaates", "open_in_openstreetmap": "Ava OpenStreetMap", "open_the_search_filters": "Ava otsingufiltrid", @@ -1810,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga", "reassing_hint": "Seosta valitud üksused olemasoleva isikuga", "recent": "Hiljutine", - "recent-albums": "Hiljutised albumid", + "recent_albums": "Hiljutised albumid", "recent_searches": "Hiljutised otsingud", "recently_added": "Hiljuti lisatud", "recently_added_page_title": "Hiljuti lisatud", @@ -2179,6 +2190,7 @@ "support": "Tugi", "support_and_feedback": "Tugi ja tagasiside", "support_third_party_description": "Sinu Immich'i install on kolmanda osapoole pakendatud. Probleemid, mida täheldad, võivad olla põhjustatud selle pakendamise poolt, seega võta esmajärjekorras nendega ühendust, kasutades allolevaid linke.", + "supporter": "Toetaja", "swap_merge_direction": "Muuda ühendamise suunda", "sync": "Sünkrooni", "sync_albums": "Sünkrooni albumid", diff --git a/i18n/fi.json b/i18n/fi.json index a47cd53933..425e7a719e 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -1568,7 +1568,6 @@ "not_available": "N/A", "not_in_any_album": "Ei yhdessäkään albumissa", "not_selected": "Ei valittu", - "note_apply_storage_label_to_previously_uploaded assets": "Huom: Jotta voit soveltaa tallennustunnistetta aiemmin ladattuihin kohteisiin, suorita", "notes": "Muistiinpanot", "nothing_here_yet": "Ei vielä mitään", "notification_permission_dialog_content": "Ottaaksesi ilmoitukset käyttöön, siirry asetuksiin ja valitse 'salli'.", @@ -1767,7 +1766,7 @@ "reassigned_assets_to_new_person": "Määritetty {count, plural, one {# media} other {# mediaa}} uudelle henkilölle", "reassing_hint": "Määritä valitut mediat käyttäjälle", "recent": "Viimeisin", - "recent-albums": "Viimeisimmät albumit", + "recent_albums": "Viimeisimmät albumit", "recent_searches": "Edelliset haut", "recently_added": "Viimeksi lisätty", "recently_added_page_title": "Viimeksi lisätyt", diff --git a/i18n/fr.json b/i18n/fr.json index 6086a3f88c..7dc9e80e21 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -86,7 +86,7 @@ "export_config_as_json_description": "Télécharger la configuration actuelle du système en tant que fichier JSON", "external_libraries_page_description": "Page d'administration des bibliothèques externes", "face_detection": "Détection des visages", - "face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » retraite tous les visages en repartant de zéro. « Manquant » met en file d'attente les médias qui n'ont pas encore été traités. Lorsque la détection est terminée, les visages détectés seront mis en file d'attente pour la reconnaissance faciale.", + "face_detection_description": "Détecte les visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » retraite tous les visages en repartant de zéro. « Manquant » met en file d'attente les médias qui n'ont pas encore été traités. Lorsque la détection est terminée, les visages détectés seront mis en file d'attente pour la reconnaissance faciale, les regroupant en personnes existantes ou nouvelles.", "facial_recognition_job_description": "Regrouper les visages détectés en personnes. Cette étape est exécutée une fois la détection des visages terminée. « Réinitialiser » (re)regroupe tous les visages. « Manquant » met en file d'attente les visages auxquels aucune personne n'a été attribuée.", "failed_job_command": "La commande {command} a échoué pour la tâche : {job}", "force_delete_user_warning": "ATTENTION : Cette opération entraîne la suppression immédiate de l'utilisateur et de tous ses médias. Cette opération ne peut être annulée et les fichiers ne peuvent être récupérés.", @@ -104,7 +104,7 @@ "image_preview_description": "Image de taille moyenne avec métadonnées retirées, utilisée lors de la visualisation d'un seul média et pour l'apprentissage automatique", "image_preview_quality_description": "Qualité de l'aperçu : de 1 à 100. Une valeur plus élevée produit de meilleurs résultats, mais elle produit des fichiers plus volumineux et peut réduire la réactivité de l'application. Une valeur trop basse peut affecter la qualité de l'apprentissage automatique.", "image_preview_title": "Paramètres de prévisualisation", - "image_progressive": "Progressive", + "image_progressive": "Progressif", "image_progressive_description": "Encode les images JPEG de manière progressive pour un affichage graduel. Cela n'a pas d'effet sur les images en WebP.", "image_quality": "Qualité", "image_resolution": "Résolution", @@ -311,7 +311,7 @@ "search_jobs": "Recherche des tâches…", "send_welcome_email": "Envoyer un courriel de bienvenue", "server_external_domain_settings": "Domaine externe", - "server_external_domain_settings_description": "Nom de domaine pour les liens partagés publics, y compris http(s)://", + "server_external_domain_settings_description": "Nom de domaine utilisé pour les liens externes", "server_public_users": "Utilisateurs publics", "server_public_users_description": "Tous les utilisateurs (nom et courriel) sont listés lors de l'ajout d'un utilisateur à des albums partagés. Quand cela est désactivé, la liste des utilisateurs est uniquement disponible pour les comptes administrateurs.", "server_settings": "Paramètres du serveur", @@ -351,7 +351,7 @@ "template_settings": "Modèles de notifications", "template_settings_description": "Gérer les modèles personnalisés pour les notifications", "theme_custom_css_settings": "CSS personnalisé", - "theme_custom_css_settings_description": "Les feuilles de style en cascade (CSS) permettent de personnaliser l'apparence d'Immich.", + "theme_custom_css_settings_description": "Les feuilles de style (CSS) permettent de personnaliser l'apparence d'Immich.", "theme_settings": "Paramètres du thème", "theme_settings_description": "Gérer la personnalisation de l'interface web d'Immich", "thumbnail_generation_job": "Génération des miniatures", @@ -782,6 +782,8 @@ "client_cert_import": "Importer", "client_cert_import_success_msg": "Certificat importé", "client_cert_invalid_msg": "Fichier de certificat invalide ou mot de passe incorrect", + "client_cert_password_message": "Renseignez le mot de passe de ce certificat", + "client_cert_password_title": "Mot de passe du certificat", "client_cert_remove_msg": "Certificat supprimé", "client_cert_subtitle": "Prend en charge uniquement le format PKCS12 (.p12, .pfx). L'importation/suppression de certificats n'est possible qu'avant la connexion", "client_cert_title": "Certificat SSL [EXPÉRIMENTAL]", @@ -792,6 +794,11 @@ "color": "Couleur", "color_theme": "Thème de couleur", "command": "Commande", + "command_palette_prompt": "Trouver rapidement des pages, actions ou commandes", + "command_palette_to_close": "pour fermer", + "command_palette_to_navigate": "pour entrer", + "command_palette_to_select": "pour sélectionner", + "command_palette_to_show_all": "pour tout afficher", "comment_deleted": "Commentaire supprimé", "comment_options": "Options des commentaires", "comments_and_likes": "Commentaires et \"J'aime\"", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "Les changements ne seront pas enregistrés", "editor_close_without_save_title": "Fermer l'éditeur ?", "editor_confirm_reset_all_changes": "Êtes-vous sûr de vouloir réinitialiser toutes les modifications ?", + "editor_discard_edits_confirm": "Annuler les éditions", + "editor_discard_edits_prompt": "Vous avez des modifications non sauvegardées. Etes-vous sûr de vouloir les perdre ?", + "editor_discard_edits_title": "Annuler les éditions ?", + "editor_edits_applied_error": "Echec d'application des éditions", + "editor_edits_applied_success": "Editions appliquées avec succès", "editor_flip_horizontal": "Retourner horizontalement", "editor_flip_vertical": "Retourner verticalement", "editor_orientation": "Orientation", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONNES", "exif_bottom_sheet_person_add_person": "Ajouter un nom", "exit_slideshow": "Quitter le diaporama", + "expand": "Développer", "expand_all": "Tout développer", "experimental_settings_new_asset_list_subtitle": "En cours de développement", "experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale", @@ -1196,6 +1209,8 @@ "features_in_development": "Fonctionnalités en développement", "features_setting_description": "Gérer les fonctionnalités de l'application", "file_name_or_extension": "Nom du fichier ou extension", + "file_name_text": "Nom du fichier", + "file_name_with_value": "Nom du fichier : {file_name}", "file_size": "Taille du fichier", "filename": "Nom du fichier", "filetype": "Type de fichier", @@ -1523,7 +1538,7 @@ "mobile_app_download_onboarding_note": "Téléchargez l'application mobile compagnon via les options suivantes", "model": "Modèle", "month": "Mois", - "monthly_title_text_date_format": "MMMM y", + "monthly_title_text_date_format": "MMMM a", "more": "Plus", "move": "Déplacer", "move_down": "Descendre", @@ -1604,7 +1619,6 @@ "not_available": "N/A", "not_in_any_album": "Dans aucun album", "not_selected": "Non sélectionné", - "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'étiquette de stockage aux médias précédemment envoyés, exécutez", "notes": "Notes", "nothing_here_yet": "Rien pour le moment", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.", @@ -1634,6 +1648,7 @@ "online": "En ligne", "only_favorites": "Uniquement les favoris", "open": "Ouvrir", + "open_calendar": "Ouvrir le calendrier", "open_in_map_view": "Montrer sur la carte", "open_in_openstreetmap": "Ouvrir dans OpenStreetMap", "open_the_search_filters": "Ouvrir les filtres de recherche", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# média réattribué} other {# médias réattribués}} à une nouvelle personne", "reassing_hint": "Attribuer ces médias à une personne existante", "recent": "Récent", - "recent-albums": "Albums récents", + "recent_albums": "Albums récents", "recent_searches": "Recherches récentes", "recently_added": "Récemment ajouté", "recently_added_page_title": "Récemment ajouté", @@ -2175,6 +2190,7 @@ "support": "Soutenir", "support_and_feedback": "Support & Retours", "support_third_party_description": "Votre installation d'Immich est packagée via une application tierce. Si vous rencontrez des anomalies, elles peuvent venir de ce packaging tiers, merci de créer les anomalies avec ces tiers en premier lieu en utilisant les liens ci-dessous.", + "supporter": "Contributeur", "swap_merge_direction": "Inverser la direction de fusion", "sync": "Synchroniser", "sync_albums": "Synchroniser dans des albums", diff --git a/i18n/ga.json b/i18n/ga.json index 464e8d9763..409cc293d6 100644 --- a/i18n/ga.json +++ b/i18n/ga.json @@ -311,7 +311,7 @@ "search_jobs": "Cuardaigh poist…", "send_welcome_email": "Seol ríomhphost fáilte", "server_external_domain_settings": "Fearann seachtrach", - "server_external_domain_settings_description": "Fearann le haghaidh naisc chomhroinnte poiblí, lena n-áirítear http(s)://", + "server_external_domain_settings_description": "Fearann a úsáidtear le haghaidh naisc sheachtracha", "server_public_users": "Úsáideoirí Poiblí", "server_public_users_description": "Liostaítear gach úsáideoir (ainm agus ríomhphost) nuair a chuirtear úsáideoir le halbaim chomhroinnte. Nuair a bhíonn sé díchumasaithe, ní bheidh an liosta úsáideoirí ar fáil ach d’úsáideoirí riarthóra.", "server_settings": "Socruithe Freastalaí", @@ -782,6 +782,8 @@ "client_cert_import": "Iompórtáil", "client_cert_import_success_msg": "Tá deimhniú cliant allmhairithe", "client_cert_invalid_msg": "Comhad teastais neamhbhailí nó pasfhocal mícheart", + "client_cert_password_message": "Cuir isteach an focal faire don deimhniú seo", + "client_cert_password_title": "Pasfhocal an Teastais", "client_cert_remove_msg": "Baineadh teastas an chliaint", "client_cert_subtitle": "Tacaíonn sé le formáid PKCS12 (.p12, .pfx) amháin. Ní féidir teastais a allmhairiú/a bhaint ach amháin roimh logáil isteach", "client_cert_title": "Teastas cliant SSL [TURGHAINNEACH]", @@ -792,6 +794,11 @@ "color": "Dath", "color_theme": "Téama datha", "command": "Ordú", + "command_palette_prompt": "Aimsigh leathanaigh, gníomhartha nó orduithe go tapa", + "command_palette_to_close": "a dhúnadh", + "command_palette_to_navigate": "dul isteach", + "command_palette_to_select": "a roghnú", + "command_palette_to_show_all": "chun gach rud a thaispeáint", "comment_deleted": "Trácht scriosta", "comment_options": "Roghanna tráchta", "comments_and_likes": "Tráchtanna & Is maith liom", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "Ní shábhálfar na hathruithe", "editor_close_without_save_title": "Dún an t-eagarthóir?", "editor_confirm_reset_all_changes": "An bhfuil tú cinnte gur mian leat na hathruithe go léir a athshocrú?", + "editor_discard_edits_confirm": "Scrios na heagarthóireachtaí", + "editor_discard_edits_prompt": "Tá eagarthóireachtaí neamhshábháilte agat. An bhfuil tú cinnte gur mhaith leat iad a chaitheamh amach?", + "editor_discard_edits_title": "Scrios na heagarthóireachtaí?", + "editor_edits_applied_error": "Theip ar na heagarthóireachtaí a chur i bhfeidhm", + "editor_edits_applied_success": "Cuireadh na heagarthóireachtaí i bhfeidhm go rathúil", "editor_flip_horizontal": "Fillte go cothrománach", "editor_flip_vertical": "Smeach ingearach", "editor_orientation": "Treoshuíomh", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "DAOINE", "exif_bottom_sheet_person_add_person": "Cuir ainm leis", "exit_slideshow": "Scoir an Taispeántais Sleamhnán", + "expand": "Leathnaigh", "expand_all": "Leathnaigh gach rud", "experimental_settings_new_asset_list_subtitle": "Obair ar siúl", "experimental_settings_new_asset_list_title": "Cumasaigh eangach grianghraf turgnamhach", @@ -1196,6 +1209,8 @@ "features_in_development": "Gnéithe i bhForbairt", "features_setting_description": "Bainistigh gnéithe an aip", "file_name_or_extension": "Ainm comhaid nó síneadh", + "file_name_text": "Ainm comhaid", + "file_name_with_value": "Ainm comhaid: {file_name}", "file_size": "Méid comhaid", "filename": "Ainm comhaid", "filetype": "Cineál comhaid", @@ -1604,7 +1619,6 @@ "not_available": "N/B", "not_in_any_album": "Ní in aon albam", "not_selected": "Níor roghnaíodh", - "note_apply_storage_label_to_previously_uploaded assets": "Nóta: Chun an Lipéad Stórála a chur i bhfeidhm ar shócmhainní a uaslódáileadh roimhe seo, rith an", "notes": "Nótaí", "nothing_here_yet": "Níl aon rud anseo fós", "notification_permission_dialog_content": "Chun fógraí a chumasú, téigh go Socruithe agus roghnaigh ceadaigh.", @@ -1634,6 +1648,7 @@ "online": "Ar líne", "only_favorites": "Is fearr leat amháin", "open": "Oscail", + "open_calendar": "Oscail an féilire", "open_in_map_view": "Oscail i radharc léarscáile", "open_in_openstreetmap": "Oscail in OpenStreetMap", "open_the_search_filters": "Oscail na scagairí cuardaigh", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "Athshannadh {count, plural, one {# sócmhainn} other {# sócmhainní}} do dhuine nua", "reassing_hint": "Sannadh sócmhainní roghnaithe do dhuine atá ann cheana féin", "recent": "Le déanaí", - "recent-albums": "Albaim le déanaí", + "recent_albums": "Albaim le déanaí", "recent_searches": "Cuardaigh le déanaí", "recently_added": "Cuireadh leis le déanaí", "recently_added_page_title": "Curtha leis le Déanaí", @@ -2175,6 +2190,7 @@ "support": "Tacaíocht", "support_and_feedback": "Tacaíocht & Aiseolas", "support_third_party_description": "Rinne tríú páirtí pacáiste de do shuiteáil Immich. D’fhéadfadh sé gur an pacáiste sin ba chúis le fadhbanna a bhíonn agat, mar sin tabhair ceisteanna dóibh ar dtús trí na naisc thíos a úsáid.", + "supporter": "Tacaíochtaí", "swap_merge_direction": "Malartaigh treo an chumaisc", "sync": "Sioncrónaigh", "sync_albums": "Sioncrónaigh albaim", diff --git a/i18n/gl.json b/i18n/gl.json index f3717259ee..135e3f64cd 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -18,6 +18,7 @@ "add_a_title": "Engadir un título", "add_action": "Engadir acción", "add_action_description": "Faga click para engadir unha acción a realizar", + "add_assets": "Engadir activos", "add_birthday": "Engadir aniversario", "add_endpoint": "Engadir punto final", "add_exclusion_pattern": "Engadir patrón de exclusión", @@ -103,6 +104,8 @@ "image_preview_description": "Imaxe de tamaño medio con metadatos eliminados, usada ao ver un único activo e para aprendizaxe automática", "image_preview_quality_description": "Calidade da vista previa de 1 a 100. Canto máis alto, mellor, pero produce ficheiros máis grandes e pode reducir a capacidade de resposta da aplicación. Establecer un valor baixo pode afectar á calidade da aprendizaxe automática.", "image_preview_title": "Configuración da vista previa", + "image_progressive": "Progresivo", + "image_progressive_description": "Codifica imaxes JPEG progresivamente para unha visualización con carga gradual. Isto non ten ningún efecto en imaxes WebP.", "image_quality": "Calidade", "image_resolution": "Resolución", "image_resolution_description": "Resolucións máis altas poden preservar máis detalles pero tardan máis en codificarse, teñen tamaños de ficheiro máis grandes e poden reducir a capacidade de resposta da aplicación.", @@ -187,10 +190,21 @@ "machine_learning_smart_search_enabled": "Activar busca intelixente", "machine_learning_smart_search_enabled_description": "Se está desactivado, as imaxes non se codificarán para a busca intelixente.", "machine_learning_url_description": "A URL do servidor de aprendizaxe automática. Se se proporciona máis dunha URL, intentarase con cada servidor un por un ata que un responda correctamente, en orde do primeiro ao último. Os servidores que non respondan ignoraranse temporalmente ata que volvan estar en liña.", + "maintenance_delete_backup": "Eliminar copia de seguridade", + "maintenance_delete_backup_description": "Este arquivo será borrado permanentemente.", + "maintenance_delete_error": "Erro ao eliminar a copia de seguridade.", + "maintenance_restore_backup": "Recuperar copia de seguridade", + "maintenance_restore_backup_description": "Immich borrarase e restaurarase desde a copia de seguridade escollida. Crearase unha copia de seguridade antes de continuar.", + "maintenance_restore_backup_different_version": "Esta copia de seguridade foi creada cunha versión diferente de Immich!", + "maintenance_restore_backup_unknown_version": "Non se puido determinal a versión da copia de seguridade.", + "maintenance_restore_database_backup": "Restaurar copia de seguridade da base de datos", + "maintenance_restore_database_backup_description": "Reverter a un estado anterior da base de datos usando unha copia de seguridade", "maintenance_settings": "Mantemento", "maintenance_settings_description": "Poñer Immich en modo mantemento.", - "maintenance_start": "Comezar modo de mantemento", + "maintenance_start": "Cambiar ao modo de mantemento", "maintenance_start_error": "Erro ao iniciar o modo de mantemento.", + "maintenance_upload_backup": "Subir arquivo de copia de seguridade", + "maintenance_upload_backup_error": "Non se puido subir a copia de seguridade, o formato é .sql/.sql.gz ?", "manage_concurrency": "Xestionar Concorrencia", "manage_concurrency_description": "Navegar á páxina de traballos para xestionar a concorrencia de trabalhos", "manage_log_settings": "Xestionar configuración de rexistro", @@ -258,7 +272,7 @@ "oauth_auto_register": "Rexistro automático", "oauth_auto_register_description": "Rexistrar automaticamente novos usuarios despois de iniciar sesión con OAuth", "oauth_button_text": "Texto do botón", - "oauth_client_secret_description": "Requirido se o provedor OAuth non admite PKCE (Proof Key for Code Exchange)", + "oauth_client_secret_description": "Requirido para clientes confidenciais ou se o provedor OAuth non admite PKCE (Proof Key for Code Exchange).", "oauth_enable_description": "Iniciar sesión con OAuth", "oauth_mobile_redirect_uri": "URI de redirección móbil", "oauth_mobile_redirect_uri_override": "Substitución de URI de redirección móbil", @@ -337,7 +351,7 @@ "template_settings": "Modelos de Notificación", "template_settings_description": "Xestionar modelos personalizados para notificacións", "theme_custom_css_settings": "CSS Personalizado", - "theme_custom_css_settings_description": "As Follas de Estilo en Cascada (CSS) permiten personalizar o deseño de Immich.", + "theme_custom_css_settings_description": "As follas de estilo en cascada (CSS) permiten personalizar o deseño de Immich.", "theme_settings": "Configuración do Tema", "theme_settings_description": "Xestionar a personalización da interface web de Immich", "thumbnail_generation_job": "Xerar Miniaturas", @@ -437,6 +451,9 @@ "admin_password": "Contrasinal do administrador", "administration": "Administración", "advanced": "Avanzado", + "advanced_settings_clear_image_cache": "Limpar a caché da imaxe", + "advanced_settings_clear_image_cache_error": "Fallo ao limpar a caché da imaxe", + "advanced_settings_clear_image_cache_success": "Caché borrada correctamente {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Use esta opción para filtrar medios durante a sincronización baseándose en criterios alternativos. Só probe isto se ten problemas coa aplicación para detectar todos os álbums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronización de álbums do dispositivo", "advanced_settings_log_level_title": "Nivel de rexistro: {level}", @@ -478,6 +495,7 @@ "album_summary": "Resumo do álbum", "album_updated": "Álbum actualizado", "album_updated_setting_description": "Recibir unha notificación por correo electrónico cando un álbum compartido teña novos activos", + "album_upload_assets": "Cargar recursos desde o teu ordenador e engadilos ao álbum", "album_user_left": "Saíu de {album}", "album_user_removed": "Eliminouse a {user}", "album_viewer_appbar_delete_confirm": "Está seguro de que quere eliminar este álbum da súa conta?", @@ -499,6 +517,7 @@ "all": "Todo", "all_albums": "Todos os álbums", "all_people": "Todas as persoas", + "all_photos": "Todas as fotos", "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir edicións", @@ -506,6 +525,9 @@ "allow_public_user_to_upload": "Permitir que o usuario público cargue", "allowed": "Permitido", "alt_text_qr_code": "Imaxe de código QR", + "always_keep": "Manter sempre", + "always_keep_photos_hint": "Liberar espazo manterá todas as fotos neste dispositivo.", + "always_keep_videos_hint": "Liberar espazo manterá todos os videos neste dispositivo.", "anti_clockwise": "Sentido antihorario", "api_key": "Chave API", "api_key_description": "Este valor só se mostrará unha vez. Asegúrese de copialo antes de pechar a xanela.", @@ -550,6 +572,9 @@ "asset_list_layout_sub_title": "Deseño", "asset_list_settings_subtitle": "Configuración do deseño da grella de fotos", "asset_list_settings_title": "Grella de Fotos", + "asset_not_found_on_device_android": "Non se atopou o recurso no dispositivo", + "asset_not_found_on_device_ios": "Non se atopou o recurso no dispositivo. Se estás a usar iCloud, é posible que non sexa posible acceder ao recurso debido a un ficheiro incorrecto almacenado en iCloud", + "asset_not_found_on_icloud": "Non se atopou o recurso en iCloud. É posible que non se poida acceder ao recurso debido a un ficheiro incorrecto almacenado en iCloud", "asset_offline": "Activo Fóra de Liña", "asset_offline_description": "Este activo externo xa non se atopa no disco. Por favor, contacte co seu administrador de Immich para obter axuda.", "asset_restored_successfully": "Activo restaurado correctamente", @@ -601,7 +626,7 @@ "backup_album_selection_page_select_albums": "Seleccionar álbums", "backup_album_selection_page_selection_info": "Información da selección", "backup_album_selection_page_total_assets": "Total de activos únicos", - "backup_albums_sync": "Sincronización de álbums da copia de seguridade", + "backup_albums_sync": "Sincronización de álbums de copia de seguridade", "backup_all": "Todo", "backup_background_service_backup_failed_message": "Erro ao facer copia de seguridade dos activos. Reintentando…", "backup_background_service_complete_notification": "Copia de seguridade dos recursos completada", @@ -734,6 +759,18 @@ "checksum": "Suma de comprobación", "choose_matching_people_to_merge": "Elixir persoas coincidentes para fusionar", "city": "Cidade", + "cleanup_confirm_description": "Immich atopou {count} recursos (creados antes de {date}) copiados de seguridade no servidor. Queres eliminar as copias locais deste dispositivo?", + "cleanup_confirm_prompt_title": "Eliminar deste dispositivo?", + "cleanup_deleted_assets": "Movéronse {count} recursos á papeleira do dispositivo", + "cleanup_deleting": "Movendo ao lixo...", + "cleanup_found_assets": "Atopáronse {count} arquivo(s) con copias de seguridade", + "cleanup_found_assets_with_size": "Atopáronse {count} recursos con copia de seguridade ({size})", + "cleanup_icloud_shared_albums_excluded": "Os álbums compartidos de iCloud están excluídos da análise", + "cleanup_no_assets_found": "Non se atoparon recursos que coincidan cos criterios anteriores. Liberar espazo só pode eliminar recursos dos que se fixo unha copia de seguridade no servidor", + "cleanup_preview_title": "Arquivos que van ser borrados ({count})", + "cleanup_step3_description": "Busca recursos con copia de seguridade que coincidan coa túa data e garda a configuración.", + "cleanup_step4_summary": "{count} arquivos (creados antes do {date}) para eliminar do teu dispositivo local. As fotos seguirán accesibles dende a aplicación de Immich.", + "cleanup_trash_hint": "Para recuperar todo o espazo de almacenamento, abre a aplicación da galería do sistema e baleira a papeleira", "clear": "Limpar", "clear_all": "Limpar todo", "clear_all_recent_searches": "Limpar todas as buscas recentes", @@ -745,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado de cliente importado", "client_cert_invalid_msg": "Ficheiro de certificado inválido ou contrasinal incorrecto", + "client_cert_password_message": "Introduza o contrasinal para este certificado", + "client_cert_password_title": "Contrasinal do certificado", "client_cert_remove_msg": "Certificado de cliente eliminado", "client_cert_subtitle": "Soporta só o formato PKCS12 (.p12, .pfx). A importación ou eliminación de certificados está dispoñible só antes de iniciar sesión", "client_cert_title": "Certificado de cliente SSL [EXPERIMENTAL]", @@ -819,13 +858,20 @@ "created_at": "Creado", "creating_linked_albums": "Creando álbums vinculados...", "crop": "Recortar", + "crop_aspect_ratio_fixed": "Fixado", + "crop_aspect_ratio_free": "Libre", + "crop_aspect_ratio_original": "Orixinal", "curated_object_page_title": "Cousas", "current_device": "Dispositivo actual", "current_pin_code": "Código PIN actual", "current_server_address": "Enderezo do servidor actual", + "custom_date": "Data personalizada", "custom_locale": "Configuración Rexional Personalizada", "custom_locale_description": "Formatar datas e números baseándose na lingua e a rexión", "custom_url": "URL personalizada", + "cutoff_date_description": "Manter fotos dos últimos…", + "cutoff_day": "{count, plural, one {day} other {days}}", + "cutoff_year": "{count, plural, one {day} other {days}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", @@ -917,6 +963,7 @@ "download_waiting_to_retry": "Agardando para reintentar", "downloading": "Descargando", "downloading_asset_filename": "Descargando activo {filename}", + "downloading_from_icloud": "Descargando dende iCloud", "downloading_media": "Descargando medios", "drop_files_to_upload": "Solte ficheiros en calquera lugar para cargar", "duplicates": "Duplicados", @@ -949,6 +996,18 @@ "editor": "Editor", "editor_close_without_save_prompt": "Os cambios non se gardarán", "editor_close_without_save_title": "Pechar editor?", + "editor_confirm_reset_all_changes": "Seguro que queres restablecer todos os cambios?", + "editor_discard_edits_confirm": "Descartar edicións", + "editor_discard_edits_prompt": "Tes cambios sen gardar. Estás seguro que queres descartalos?", + "editor_discard_edits_title": "Descartar cambios?", + "editor_edits_applied_error": "Non se puideron aplicar as edicións", + "editor_edits_applied_success": "As edicións aplicáronse correctamente", + "editor_flip_horizontal": "Xirar horizontalmente", + "editor_flip_vertical": "Xirar verticalmente", + "editor_orientation": "Orientación", + "editor_reset_all_changes": "Restablecer os cambios", + "editor_rotate_left": "Xirar 90° en sentido antihorario", + "editor_rotate_right": "Xirar 90° en sentido horario", "email": "Correo electrónico", "email_notifications": "Notificacións por correo electrónico", "empty_folder": "Este cartafol está baleiro", @@ -967,11 +1026,14 @@ "error_change_sort_album": "Erro ao cambiar a orde de clasificación do álbum", "error_delete_face": "Erro ao eliminar a cara do activo", "error_getting_places": "Erro ao obter lugares", + "error_loading_albums": "Produciuse un erro ao cargar os álbums", "error_loading_image": "Erro ao cargar a imaxe", "error_loading_partners": "Erro cargando compañeiros/as: {error}", + "error_retrieving_asset_information": "Produciuse un erro ao recuperar a información do recurso", "error_saving_image": "Erro: {error}", "error_tag_face_bounding_box": "Erro ao etiquetar cara - non se poden obter as coordenadas da caixa delimitadora", "error_title": "Erro - Algo saíu mal", + "error_while_navigating": "Produciuse un erro ao navegar ata o recurso", "errors": { "cannot_navigate_next_asset": "Non se pode navegar ao seguinte activo", "cannot_navigate_previous_asset": "Non se pode navegar ao activo anterior", @@ -1080,6 +1142,7 @@ "unable_to_scan_library": "Non se puido escanear a biblioteca", "unable_to_set_feature_photo": "Non se puido establecer a foto destacada", "unable_to_set_profile_picture": "Non se puido establecer a imaxe de perfil", + "unable_to_set_rating": "Non se pode definir a clasificación", "unable_to_submit_job": "Non se puido enviar o traballo", "unable_to_trash_asset": "Non se puido mover o activo ao lixo", "unable_to_unlink_account": "Non se puido desvincular a conta", @@ -1140,6 +1203,8 @@ "features_in_development": "Funcionalidades en Desenvolvemento", "features_setting_description": "Xestionar as funcións da aplicación", "file_name_or_extension": "Nome do ficheiro ou extensión", + "file_name_text": "Nome do arquivo", + "file_name_with_value": "Nome do arquivo: {file_name}", "file_size": "Tamaño do arquivo", "filename": "Nome do ficheiro", "filetype": "Tipo de ficheiro", @@ -1157,6 +1222,9 @@ "folders_feature_description": "Navegar pola vista de cartafoles para as fotos e vídeos no sistema de ficheiros", "forgot_pin_code_question": "Esqueceu o seu PIN?", "forward": "Adiante", + "free_up_space": "Liberar espazo", + "free_up_space_description": "Move as fotos e os vídeos dos que fixeches unha copia de seguridade á papeleira do teu dispositivo para liberar espazo. As túas copias no servidor permanecen seguras.", + "free_up_space_settings_subtitle": "Liberar almacenamento do dispositivo", "full_path": "Ruta completa: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade carga recursos externos de Google para poder funcionar.", @@ -1272,8 +1340,15 @@ "json_editor": "Editor JSON", "json_error": "Erro JSON", "keep": "Conservar", + "keep_albums": "Gardar álbums", + "keep_albums_count": "Manter {count} {count, plural, one {album} other {albums}}", "keep_all": "Conservar Todo", + "keep_description": "Escolle o que permanece no teu dispositivo ao liberar espazo.", + "keep_favorites": "Manter favoritos", + "keep_on_device": "Manter no dispositivo", + "keep_on_device_hint": "Seleccionar elementos a manter neste dispositivo", "keep_this_delete_others": "Conservar este, eliminar outros", + "keeping": "Mantendo: {items}", "kept_this_deleted_others": "Conservouse este activo e elimináronse {count, plural, one {# activo} other {# activos}}", "keyboard_shortcuts": "Atallos de teclado", "language": "Lingua", @@ -1367,10 +1442,28 @@ "loop_videos_description": "Activar para reproducir automaticamente un vídeo en bucle no visor de detalles.", "main_branch_warning": "Está a usar unha versión de desenvolvemento; recomendamos encarecidamente usar unha versión de lanzamento!", "main_menu": "Menú principal", + "maintenance_action_restore": "Restablecendo base de datos", "maintenance_description": "Immich foi posto en modo de mantemento.", "maintenance_end": "Finalizar o modo de mantemento", "maintenance_end_error": "Erro ao finalizar o modo de mantemento.", "maintenance_logged_in_as": "Sesión iniciada actualmente como {user}", + "maintenance_restore_from_backup": "Restaurar dende unha copia de seguridade", + "maintenance_restore_library": "Restaura a túa librería", + "maintenance_restore_library_confirm": "Se isto parece correcto, continúa restaurando unha copia de seguridade!", + "maintenance_restore_library_description": "Restaurando copia de seguridade", + "maintenance_restore_library_folder_has_files": "{folder} ten {count} carpeta(s)", + "maintenance_restore_library_folder_no_files": "Faltan arquivos en {folder}!", + "maintenance_restore_library_folder_pass": "lexible e escribible", + "maintenance_restore_library_folder_read_fail": "non lexible", + "maintenance_restore_library_folder_write_fail": "non escribible", + "maintenance_restore_library_hint_missing_files": "Pode que che falten ficheiros importantes", + "maintenance_restore_library_hint_regenerate_later": "Podes rexeneralos máis tarde na configuración", + "maintenance_restore_library_hint_storage_template_missing_files": "Usas un modelo de almacenamento? Pode que che falten arquivos", + "maintenance_restore_library_loading": "Cargando comprobacións de integridade e heurísticas…", + "maintenance_task_backup": "Creando unha copia de seguridade da base de datos existente…", + "maintenance_task_migrations": "Executando migracións de bases de datos…", + "maintenance_task_restore": "Restaurando a copia de seguridade escollida…", + "maintenance_task_rollback": "Fallou a restauración, volvendo ao punto de restauración…", "maintenance_title": "Non dispoñible temporalmente", "make": "Marca", "manage_geolocation": "Xestionar a localización", @@ -1432,6 +1525,8 @@ "minimize": "Minimizar", "minute": "Minuto", "minutes": "Minutos", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Faltantes", "mobile_app": "Aplicación Móbil", "mobile_app_download_onboarding_note": "Descarga a aplicación móbil complementaria usando as seguintes opcións", @@ -1443,6 +1538,7 @@ "move_down": "Baixar", "move_off_locked_folder": "Mover fóra do cartafol bloqueado", "move_to": "Mover a", + "move_to_device_trash": "Mover á papeleira do dispositivo", "move_to_lock_folder_action_prompt": "{count} engadido/a ao cartafol bloqueado", "move_to_locked_folder": "Mover ao cartafol bloqueado", "move_to_locked_folder_confirmation": "Estas fotos e vídeo eliminaranse de todos os álbums e só serán visíbeis dende o cartafol bloqueado", @@ -1482,11 +1578,12 @@ "next_memory": "Seguinte recordo", "no": "Non", "no_actions_added": "Non hai accións engadidas polo momento", + "no_albums_found": "Non se atoparon álbums", "no_albums_message": "Cree un álbum para organizar as súas fotos e vídeos", "no_albums_with_name_yet": "Parece que aínda non ten ningún álbum con este nome.", "no_albums_yet": "Parece que aínda non ten ningún álbum.", "no_archived_assets_message": "Arquive fotos e vídeos para ocultalos da súa vista de Fotos", - "no_assets_message": "PREMA PARA CARGAR A SÚA PRIMEIRA FOTO", + "no_assets_message": "Fai clic para subir a túa primeira foto", "no_assets_to_show": "Non hai activos para mostrar", "no_cast_devices_found": "Non se atoparon dispositivos de transmisión", "no_checksum_local": "Non hai suma de verificación dispoñible - non se poden obter os activos locais", @@ -1511,11 +1608,11 @@ "no_results_description": "Probe cun sinónimo ou palabra chave máis xeral", "no_shared_albums_message": "Cree un álbum para compartir fotos e vídeos con persoas na súa rede", "no_uploads_in_progress": "Non hai cargas en curso", + "none": "Nada", "not_allowed": "Non permitido", "not_available": "Non dispoñible", "not_in_any_album": "Non está en ningún álbum", "not_selected": "Non seleccionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar a Etiqueta de Almacenamento a activos cargados previamente, execute o", "notes": "Notas", "nothing_here_yet": "Aínda nada por aquí", "notification_permission_dialog_content": "Para activar as notificacións, vaia a Axustes e seleccione permitir.", @@ -1625,6 +1722,7 @@ "photos_and_videos": "Fotos e Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de anos anteriores", + "photos_only": "Só fotos", "pick_a_location": "Elixir unha localización", "pick_custom_range": "Rango personalizado", "pick_date_range": "Seleccionar un rango de datas", @@ -1700,10 +1798,12 @@ "purchase_settings_server_activated": "A chave do produto do servidor é xestionada polo administrador", "query_asset_id": "Consultar o ID do activo", "queue_status": "Pondo en cola {count}/{total}", - "rating": "Clasificación por estrelas", + "rate_asset": "Clasificar activo", + "rating": "Valoración", "rating_clear": "Borrar clasificación", "rating_count": "{count, plural, one {# estrela} other {# estrelas}}", "rating_description": "Mostrar a clasificación EXIF no panel de información", + "rating_set": "Clasificación definida en {rating, plural, one {# star} other {# stars}}", "reaction_options": "Opcións de reacción", "read_changelog": "Ler Rexistro de Cambios", "readonly_mode_disabled": "Modo só lectura desactivado", @@ -1714,7 +1814,7 @@ "reassigned_assets_to_new_person": "Reasignados {count, plural, one {# activo} other {# activos}} a unha nova persoa", "reassing_hint": "Asignar activos seleccionados a unha persoa existente", "recent": "Recente", - "recent-albums": "Álbums recentes", + "recent_albums": "Álbums recentes", "recent_searches": "Buscas recentes", "recently_added": "Engadido recentemente", "recently_added_page_title": "Engadido Recentemente", @@ -1803,9 +1903,11 @@ "saved_settings": "Configuración gardada", "say_something": "Dicir algo", "scaffold_body_error_occurred": "Ocorreu un erro", + "scan": "Escanear", "scan_all_libraries": "Escanear Todas as Bibliotecas", "scan_library": "Escanear", "scan_settings": "Configuración de Escaneo", + "scanning": "Escaneando", "scanning_for_album": "Escaneando álbum...", "search": "Buscar", "search_albums": "Buscar álbums", @@ -1835,6 +1937,7 @@ "search_filter_media_type_title": "Seleccionar tipo de medio", "search_filter_ocr": "Buscar por OCR", "search_filter_people_title": "Seleccionar persoas", + "search_filter_star_rating": "Clasificación por estrelas", "search_for": "Buscar por", "search_for_existing_person": "Buscar persoa existente", "search_no_more_result": "Non hai máis resultados", @@ -1877,6 +1980,7 @@ "select_all_in": "Seleccionar todo en {group}", "select_avatar_color": "Seleccionar cor do avatar", "select_count": "{count, plural, one {Seleccionar #} other {Seleccionar #}}", + "select_cutoff_date": "Seleccionar data límite", "select_face": "Seleccionar cara", "select_featured_photo": "Seleccionar foto destacada", "select_from_computer": "Seleccionar do ordenador", @@ -2038,6 +2142,8 @@ "skip_to_folders": "Saltar a cartafoles", "skip_to_tags": "Saltar a etiquetas", "slideshow": "Presentación", + "slideshow_repeat": "Repetir presentación de diapositivas", + "slideshow_repeat_description": "Volver ao principio ao rematar a presentación de diapositivas", "slideshow_settings": "Configuración da presentación", "sort_albums_by": "Ordenar álbums por...", "sort_created": "Data de creación", @@ -2114,6 +2220,7 @@ "theme_setting_theme_subtitle": "Elixir a configuración do tema da aplicación", "theme_setting_three_stage_loading_subtitle": "A carga en tres etapas pode aumentar o rendemento da carga pero causa unha carga de rede significativamente maior", "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", + "then": "Entón", "they_will_be_merged_together": "Fusionaranse xuntos", "third_party_resources": "Recursos de Terceiros", "time": "Hora", @@ -2169,6 +2276,7 @@ "unhide_person": "Mostrar persoa", "unknown": "Descoñecido", "unknown_country": "País Descoñecido", + "unknown_date": "Data descoñecida", "unknown_year": "Ano Descoñecido", "unlimited": "Ilimitado", "unlink_motion_video": "Desvincular vídeo en movemento", @@ -2197,6 +2305,7 @@ "upload_details": "Detalles da Carga", "upload_dialog_info": "Quere facer copia de seguridade do(s) Activo(s) seleccionado(s) no servidor?", "upload_dialog_title": "Subir Activo", + "upload_error_with_count": "Erro de subida para {count, plural, one {# asset} other {# assets}}", "upload_errors": "Subida completada con {count, plural, one {# erro} other {# erros}}. Actualice a páxina para ver os novos activos subidos.", "upload_finished": "Carga finalizada", "upload_progress": "Restantes {remaining, number} - Procesados {processed, number}/{total, number}", @@ -2211,7 +2320,7 @@ "url": "URL", "usage": "Uso", "use_biometric": "Usar biometría", - "use_current_connection": "usar conexión actual", + "use_current_connection": "Empregar conexión actual", "use_custom_date_range": "Usar rango de datas personalizado no seu lugar", "user": "Usuario", "user_has_been_deleted": "Este usuario foi eliminado.", @@ -2244,6 +2353,7 @@ "video_hover_setting_description": "Reproducir miniatura do vídeo cando o rato está sobre o elemento. Mesmo cando está desactivado, a reprodución pode iniciarse pasando o rato sobre a icona de reprodución.", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", + "videos_only": "Só vídeos", "view": "Ver", "view_album": "Ver Álbum", "view_all": "Ver Todo", @@ -2293,6 +2403,7 @@ "yes": "Si", "you_dont_have_any_shared_links": "Non ten ningunha ligazón compartida", "your_wifi_name": "O nome da súa wifi", + "zero_to_clear_rating": "preme 0 para borrar a cualificación do activo", "zoom_image": "Ampliar Imaxe", "zoom_to_bounds": "Axustar ao perímetro" } diff --git a/i18n/gsw.json b/i18n/gsw.json index 0d8b7abf3a..17f8171c60 100644 --- a/i18n/gsw.json +++ b/i18n/gsw.json @@ -1491,7 +1491,6 @@ "not_available": "N/A", "not_in_any_album": "I keinem Album", "not_selected": "Nöd usgwählt", - "note_apply_storage_label_to_previously_uploaded assets": "Hiwiis: Zum e Spycherpfad-Bezeichnig aawehde, start de", "notes": "Notize", "nothing_here_yet": "No nüt do", "notification_permission_dialog_content": "Zum Benachrichtige aktiviere, navigier zu Iistellige und drück \"Erlaube\".", diff --git a/i18n/he.json b/i18n/he.json index 7884cea268..e4d534693b 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -1491,7 +1491,6 @@ "not_available": "לא רלוונטי", "not_in_any_album": "לא בשום אלבום", "not_selected": "לא נבחרו", - "note_apply_storage_label_to_previously_uploaded assets": "הערה: כדי להחיל את תווית האחסון על תמונות שהועלו בעבר, הפעל את", "notes": "הערות", "nothing_here_yet": "אין כאן כלום עדיין", "notification_permission_dialog_content": "כדי לאפשר התראות, לך להגדרות המכשיר ובחר אפשר.", @@ -1687,7 +1686,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {תמונה # הוקצתה} other {# תמונות הוקצו}} מחדש לאדם חדש", "reassing_hint": "הקצאת תמונות שנבחרו לאדם קיים", "recent": "חדש", - "recent-albums": "אלבומים אחרונים", + "recent_albums": "אלבומים אחרונים", "recent_searches": "חיפושים אחרונים", "recently_added": "נוסף לאחרונה", "recently_added_page_title": "נוסף לאחרונה", diff --git a/i18n/hi.json b/i18n/hi.json index 959a3aaf73..ff05291cef 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -56,7 +56,7 @@ "authentication_settings_reenable": "पुनः सक्षम करने के लिए, Server Command का प्रयोग करे।", "background_task_job": "पृष्ठभूमि कार्य", "backup_database": "डेटाबेस डंप बनाएं", - "backup_database_enable_description": "Enable database dumps", + "backup_database_enable_description": "डेटाबेस डंप चालू करें", "backup_keep_last_amount": "रखने के लिए पिछले डंप की मात्रा", "backup_onboarding_1_description": "क्लाउड में या किसी अन्य भौतिक स्थान पर ऑफसाइट प्रतिलिपि।", "backup_onboarding_2_description": "विभिन्न उपकरणों पर स्थानीय प्रतियाँ। इसमें मुख्य फ़ाइलें और उन फ़ाइलों का स्थानीय बैकअप शामिल है।", @@ -104,6 +104,8 @@ "image_preview_description": "मेटाडेटा रहित मध्यम आकार की छवि, जिसका उपयोग एकल संपत्ति देखने और मशीन लर्निंग के लिए होता है", "image_preview_quality_description": "पूर्वावलोकन की गुणवत्ता (1 से 100 तक)। अधिक मान बेहतर गुणवत्ता देता है, लेकिन इससे फ़ाइल का आकार बढ़ता है और ऐप की प्रतिक्रिया क्षमता कम हो सकती है। बहुत कम मान मशीन लर्निंग की गुणवत्ता को प्रभावित कर सकता है।", "image_preview_title": "पूर्वदर्शन सेटिंग्स", + "image_progressive": "प्रगतिशील", + "image_progressive_description": "JPEG छवियों को क्रमिक रूप से लोड करने के लिए उन्हें प्रोग्रेसिवली एनकोड करें। इसका WebP छवियों पर कोई प्रभाव नहीं पड़ता है।", "image_quality": "गुणवत्ता", "image_resolution": "रिज़ॉल्यूशन", "image_resolution_description": "उच्चतर रिज़ॉल्यूशन अधिक विवरण सुरक्षित रख सकता है, लेकिन एन्कोड करने में अधिक समय लेता है, फ़ाइल आकार बड़ा होता है और ऐप की प्रतिक्रियाशीलता कम हो सकती है।", @@ -188,11 +190,23 @@ "machine_learning_smart_search_enabled": "स्मार्ट खोज सक्षम करें", "machine_learning_smart_search_enabled_description": "यदि अक्षम किया गया है, तो स्मार्ट खोज के लिए छवियों को एन्कोड नहीं किया जाएगा।", "machine_learning_url_description": "मशीन लर्निंग सर्वर का URL। यदि एक से अधिक URL दिए गए हैं, तो प्रत्येक सर्वर को एक-एक करके कोशिश किया जाएगा, पहले से आखिरी तक, जब तक कोई सफलतापूर्वक प्रतिक्रिया न दे। जो सर्वर प्रतिक्रिया नहीं देते, उन्हें अस्थायी रूप से नजरअंदाज किया जाएगा जब तक वे फिर से ऑनलाइन न हों।", + "maintenance_delete_backup": "बैकअप डिलीट करें", + "maintenance_delete_backup_description": "यह फ़ाइल स्थायी रूप से मिटा दी जाएगी। इसे वापस नहीं लाया जा सकेगा।", + "maintenance_delete_error": "बैकअप मिटाया नहीं जा सका।", + "maintenance_restore_backup": "बैकअप वापस लाएँ", + "maintenance_restore_backup_description": "Immich का सारा डेटा पूरी तरह मिटा दिया जाएगा और चुने गए बैकअप से डेटा वापस लाया जाएगा। आगे बढ़ने से पहले एक नया बैकअप बनाया जाएगा।", + "maintenance_restore_backup_different_version": "यह बैकअप Immich के किसी अलग version में बनाया गया था!", + "maintenance_restore_backup_unknown_version": "बैकअप का version निर्धारित नहीं किया जा सका।", + "maintenance_restore_database_backup": "डेटाबेस बैकअप वापस लाएँ", + "maintenance_restore_database_backup_description": "बैकअप फ़ाइल का उपयोग करके डेटाबेस को पहले की स्थिति में वापस लाएँ", "maintenance_settings": "रखरखाव", "maintenance_settings_description": "Immich को मेंटेनेंस मोड में रखें।", - "maintenance_start": "रखरखाव मोड शुरू करें", + "maintenance_start": "रखरखाव मोड पर स्विच करें", "maintenance_start_error": "मेंटेनेंस मोड शुरू नहीं हो सका।", + "maintenance_upload_backup": "डेटाबेस की बैकअप फ़ाइल अपलोड करें", + "maintenance_upload_backup_error": "बैकअप अपलोड नहीं किया जा सका। क्या यह .sql या .sql.gz फ़ाइल है?", "manage_concurrency": "समवर्तीता प्रबंधित करें", + "manage_concurrency_description": "एक साथ चलने वाले जॉब्स का प्रबंधन करने के लिए जॉब्स पेज पर जाएँ", "manage_log_settings": "लॉग सेटिंग प्रबंधित करें", "map_dark_style": "डार्क शैली", "map_enable_description": "मानचित्र सुविधाएँ सक्षम करें", @@ -258,7 +272,7 @@ "oauth_auto_register": "ऑटो रजिस्टर", "oauth_auto_register_description": "OAuth के साथ साइन इन करने के बाद स्वचालित रूप से नए उपयोगकर्ताओं को पंजीकृत करें", "oauth_button_text": "टेक्स्ट बटन", - "oauth_client_secret_description": "यदि PKCE (कोड एक्सचेंज के लिए प्रूफ़ कुंजी) OAuth प्रदाता द्वारा समर्थित नहीं है तो यह आवश्यक है", + "oauth_client_secret_description": "यह Confidential (गोपनीय) क्लाइंट के लिए आवश्यक है, या यदि Public क्लाइंट में PKCE (Proof Key for Code Exchange) समर्थित नहीं है।", "oauth_enable_description": "OAuth से लॉगिन करें", "oauth_mobile_redirect_uri": "मोबाइल रीडायरेक्ट यूआरआई", "oauth_mobile_redirect_uri_override": "मोबाइल रीडायरेक्ट यूआरआई ओवरराइड", @@ -282,10 +296,14 @@ "password_settings_description": "पासवर्ड लॉगिन सेटिंग प्रबंधित करें", "paths_validated_successfully": "सभी पथ सफलतापूर्वक मान्य किए गए", "person_cleanup_job": "व्यक्ति सफ़ाई", + "queue_details": "प्रक्रिया कतार का विवरण", + "queues": "कार्य कतार", + "queues_page_description": "प्रशासक कार्य कतार पेज", "quota_size_gib": "कोटा आकार (GiB)", "refreshing_all_libraries": "सभी पुस्तकालयों को ताज़ा किया जा रहा है", - "registration": "व्यवस्थापक पंजीकरण", + "registration": "प्रशासक पंजीकरण", "registration_description": "चूंकि आप सिस्टम पर पहले उपयोगकर्ता हैं, इसलिए आपको व्यवस्थापक के रूप में नियुक्त किया जाएगा और आप प्रशासनिक कार्यों के लिए जिम्मेदार होंगे, और अतिरिक्त उपयोगकर्ता आपके द्वारा बनाए जाएंगे।", + "remove_failed_jobs": "असफल कार्य हटाएँ", "require_password_change_on_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है", "reset_settings_to_default": "सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें", "reset_settings_to_recent_saved": "सेटिंग्स को हाल ही में सहेजी गई सेटिंग्स पर रीसेट करें", @@ -298,8 +316,10 @@ "server_public_users_description": "साझा एल्बम में उपयोगकर्ता जोड़ते समय सभी उपयोगकर्ताओं (नाम और ईमेल) की सूची दिखाई जाती है। यदि यह विकल्प अक्षम किया गया है, तो उपयोगकर्ता सूची केवल व्यवस्थापक (एडमिन) उपयोगकर्ताओं के लिए उपलब्ध होगी।", "server_settings": "सर्वर सेटिंग्स", "server_settings_description": "सर्वर सेटिंग्स प्रबंधित करें", + "server_stats_page_description": "प्रशासक (Admin) सर्वर आँकड़े पेज", "server_welcome_message": "स्वागत संदेश", "server_welcome_message_description": "एक संदेश जो लॉगिन पृष्ठ पर प्रदर्शित होता है।", + "settings_page_description": "प्रशासक (Admin) सेटिंग्स पेज", "sidecar_job": "साइडकार मेटाडेटा", "sidecar_job_description": "फ़ाइल सिस्टम से साइडकार मेटाडेटा खोजें या सिंक्रनाइज़ करें", "slideshow_duration_description": "प्रत्येक छवि को प्रदर्शित करने के लिए सेकंड की संख्या", @@ -418,6 +438,8 @@ "user_restore_scheduled_removal": "उपयोगकर्ता को पुनर्स्थापित करें - {date, date, long} पर हटाया जाना निर्धारित है", "user_settings": "उपयोगकर्ता सेटिंग", "user_settings_description": "उपयोगकर्ता सेटिंग प्रबंधित करें", + "user_successfully_removed": "उपयोगकर्ता {email} को सफलतापूर्वक हटा दिया गया है।", + "users_page_description": "प्रशासक (Admin) उपयोगकर्ता पेज", "version_check_enabled_description": "नई रिलीज़ की जाँच के लिए GitHub पर आवधिक अनुरोध सक्षम करें", "version_check_implications": "संस्करण जाँच सुविधा github.com के साथ आवधिक संचार पर निर्भर करती है", "version_check_settings": "संस्करण चेक", @@ -429,6 +451,9 @@ "admin_password": "व्यवस्थापक पासवर्ड", "administration": "प्रशासन", "advanced": "विकसित", + "advanced_settings_clear_image_cache": "इमेज कैश (cache) साफ़ करें", + "advanced_settings_clear_image_cache_error": "इमेज कैश (cache) साफ़ नहीं किया जा सका", + "advanced_settings_clear_image_cache_success": "{size} सफलतापूर्वक साफ़ किया गया", "advanced_settings_enable_alternate_media_filter_subtitle": "सिंक के दौरान वैकल्पिक मानदंडों के आधार पर मीडिया को फ़िल्टर करने के लिए इस विकल्प का उपयोग करें। इसे केवल तभी आज़माएँ जब आपको ऐप द्वारा सभी एल्बमों का पता लगाने में समस्या हो।", "advanced_settings_enable_alternate_media_filter_title": "[प्रयोगात्मक] वैकल्पिक डिवाइस एल्बम सिंक फ़िल्टर का उपयोग करें", "advanced_settings_log_level_title": "लॉग स्तर:{level}", @@ -465,10 +490,12 @@ "album_remove_user": "उपयोगकर्ता हटाएं?", "album_remove_user_confirmation": "क्या आप वाकई {user} को हटाना चाहते हैं?", "album_search_not_found": "आपकी खोज से मेल खाता कोई एल्बम नहीं मिला", + "album_selected": "एल्बम चुना गया", "album_share_no_users": "ऐसा लगता है कि आपने यह एल्बम सभी उपयोगकर्ताओं के साथ साझा कर दिया है या आपके पास साझा करने के लिए कोई उपयोगकर्ता नहीं है।", "album_summary": "एल्बम सारांश", "album_updated": "एल्बम अपडेट किया गया", "album_updated_setting_description": "जब किसी साझा एल्बम में नई संपत्तियाँ हों तो एक ईमेल सूचना प्राप्त करें", + "album_upload_assets": "अपने कंप्यूटर से मीडिया फ़ाइलें अपलोड करें और उन्हें एल्बम में जोड़ें", "album_user_left": "बायाँ {album}", "album_user_removed": "{user} को हटाया गया", "album_viewer_appbar_delete_confirm": "क्या आप वाकई इस एल्बम को अपने खाते से हटाना चाहते हैं?", @@ -481,14 +508,16 @@ "album_viewer_page_share_add_users": "उपयोगकर्ता जोड़ें", "album_with_link_access": "लिंक वाले किसी भी व्यक्ति को इस एल्बम में फ़ोटो और लोगों को देखने दें।", "albums": "एलबम", - "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}", + "albums_count": "{count, plural, one {{count, number} एल्बम} other {{count, number} एल्बम}}", "albums_default_sort_order": "डिफ़ॉल्ट एल्बम सॉर्ट क्रम", "albums_default_sort_order_description": "नये एल्बम बनाते समय आरंभिक परिसंपत्ति सॉर्ट क्रम।", "albums_feature_description": "परिसंपत्तियों का संग्रह जिसे अन्य उपयोगकर्ताओं के साथ साझा किया जा सकता है।", "albums_on_device_count": "डिवाइस पर एल्बम ({count})", + "albums_selected": "{count, plural, one {# एल्बम चुना गया} other {# एल्बम चुने गए}}", "all": "सभी", "all_albums": "सभी एलबम", "all_people": "सभी लोग", + "all_photos": "सभी फ़ोटो", "all_videos": "सभी वीडियो", "allow_dark_mode": "डार्क मोड की अनुमति दें", "allow_edits": "संपादन की अनुमति दें", @@ -496,6 +525,9 @@ "allow_public_user_to_upload": "सार्वजनिक उपयोगकर्ता को अपलोड करने की अनुमति दें", "allowed": "अनुमत", "alt_text_qr_code": "क्यूआर कोड छवि", + "always_keep": "हमेशा रखें", + "always_keep_photos_hint": "“फ्री अप स्पेस” का उपयोग करने पर इस डिवाइस की सभी फ़ोटो बनी रहेंगी।", + "always_keep_videos_hint": "“फ्री अप स्पेस” का उपयोग करने पर इस डिवाइस की सभी वीडियो बनी रहेंगी।", "anti_clockwise": "वामावर्त", "api_key": "एपीआई की", "api_key_description": "यह की केवल एक बार दिखाई जाएगी। विंडो बंद करने से पहले कृपया इसे कॉपी करना सुनिश्चित करें।।", @@ -522,10 +554,12 @@ "archived_count": "{count, plural, other {# संग्रहीत किए गए}}", "are_these_the_same_person": "क्या ये वही व्यक्ति हैं?", "are_you_sure_to_do_this": "क्या आप वास्तव में इसे करना चाहते हैं?", + "array_field_not_fully_supported": "Array फ़ील्ड के लिए JSON को मैन्युअल रूप से संपादित करना आवश्यक है", "asset_action_delete_err_read_only": "केवल पढ़ने योग्य परिसंपत्ति(ओं) को हटाया नहीं जा सकता, छोड़ा जा सकता है", "asset_action_share_err_offline": "ऑफ़लाइन परिसंपत्ति(एँ) प्राप्त नहीं की जा सकती, छोड़ी जा रही है", "asset_added_to_album": "एल्बम में डाला गया", "asset_adding_to_album": "एल्बम में डाला जा रहा है…", + "asset_created": "एसेट बनाया गया", "asset_description_updated": "संपत्ति विवरण अद्यतन कर दिया गया है", "asset_filename_is_offline": "एसेट {filename} ऑफ़लाइन है", "asset_has_unassigned_faces": "एसेट में अनिर्धारित चेहरे हैं", @@ -538,6 +572,9 @@ "asset_list_layout_sub_title": "लेआउट", "asset_list_settings_subtitle": "फ़ोटो ग्रिड लेआउट सेटिंग्स", "asset_list_settings_title": "चित्र की जाली", + "asset_not_found_on_device_android": "डिवाइस पर एसेट नहीं मिला", + "asset_not_found_on_device_ios": "डिवाइस पर एसेट नहीं मिला। यदि आप iCloud का उपयोग कर रहे हैं, तो iCloud में खराब फ़ाइल होने के कारण एसेट तक पहुँचा नहीं जा सकता", + "asset_not_found_on_icloud": "iCloud पर एसेट नहीं मिला। iCloud में खराब फ़ाइल होने के कारण एसेट तक पहुँचा नहीं जा सकता", "asset_offline": "संपत्ति ऑफ़लाइन", "asset_offline_description": "यह संपत्ति ऑफ़लाइन है।", "asset_restored_successfully": "संपत्ति(याँ) सफलतापूर्वक पुनर्स्थापित की गईं", @@ -589,7 +626,7 @@ "backup_album_selection_page_select_albums": "एल्बम चुनें", "backup_album_selection_page_selection_info": "चयन जानकारी", "backup_album_selection_page_total_assets": "कुल अद्वितीय संपत्तियाँ", - "backup_albums_sync": "बैकअप एल्बम का तुल्यकालन", + "backup_albums_sync": "बैकअप एल्बम का सिंक्रोनाइज़ेशन", "backup_all": "सभी", "backup_background_service_backup_failed_message": "संपत्तियों का बैकअप लेने में विफल. पुनः प्रयास किया जा रहा है…", "backup_background_service_complete_notification": "एसेट का बैकअप पूरा हुआ", @@ -650,6 +687,7 @@ "backup_options_page_title": "बैकअप विकल्प", "backup_setting_subtitle": "पृष्ठभूमि और अग्रभूमि अपलोड सेटिंग प्रबंधित करें", "backup_settings_subtitle": "अपलोड सेटिंग्स संभालें", + "backup_upload_details_page_more_details": "अधिक जानकारी के लिए टैप करें", "backward": "पिछला", "biometric_auth_enabled": "बायोमेट्रिक प्रमाणीकरण सक्षम", "biometric_locked_out": "आप बायोमेट्रिक प्रमाणीकरण से बाहर हैं", @@ -708,6 +746,8 @@ "change_password_form_password_mismatch": "सांकेतिक शब्द मेल नहीं खाते", "change_password_form_reenter_new_password": "नया पासवर्ड पुनः दर्ज करें", "change_pin_code": "पिन कोड बदलें", + "change_trigger": "ट्रिगर बदलें", + "change_trigger_prompt": "क्या आप वाकई ट्रिगर बदलना चाहते हैं? इससे सभी मौजूदा एक्शन और फ़िल्टर हटा दिए जाएँगे।", "change_your_password": "अपना पासवर्ड बदलें", "changed_visibility_successfully": "दृश्यता सफलतापूर्वक परिवर्तित", "charging": "चार्जिंग", @@ -716,8 +756,21 @@ "check_corrupt_asset_backup_button": "जाँच करें", "check_corrupt_asset_backup_description": "यह जाँच केवल वाई-फ़ाई पर ही करें और सभी संपत्तियों का बैकअप लेने के बाद ही करें। इस प्रक्रिया में कुछ मिनट लग सकते हैं।", "check_logs": "लॉग जांचें", + "checksum": "चेकसम (checksum)", "choose_matching_people_to_merge": "मर्ज करने के लिए मिलते-जुलते लोगों को चुनें", "city": "शहर", + "cleanup_confirm_description": "Immich ने {date} से पहले बनाए गए {count} एसेट सर्वर पर सुरक्षित रूप से बैकअप किए हुए पाए हैं। क्या इस डिवाइस से उनकी स्थानीय प्रतियाँ हटाई जाएँ?", + "cleanup_confirm_prompt_title": "क्या इस डिवाइस से हटाएँ?", + "cleanup_deleted_assets": "डिवाइस के ट्रैश में {count} एसेट भेज दिए गए", + "cleanup_deleting": "ट्रैश में भेजा जा रहा है…", + "cleanup_found_assets": "{count} बैकअप किए गए ऐसेट मिले", + "cleanup_found_assets_with_size": "{count} बैकअप किए गए ऐसेट मिले ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud के शेयर किए गए एल्बम स्कैन में शामिल नहीं हैं", + "cleanup_no_assets_found": "ऊपर दिए गए मानदंडों से मेल खाने वाले कोई ऐसेट नहीं मिले। ‘फ्री उप स्पेस’ केवल उन्हीं ऐसेट को हटा सकता है जिनका बैकअप सर्वर पर लिया गया है", + "cleanup_preview_title": "हटाए जाने वाले ऐसेट ({count})", + "cleanup_step3_description": "तिथि और सुरक्षित रखने की सेटिंग के अनुसार बैकअप ऐसेट स्कैन करें।", + "cleanup_step4_summary": "आपके स्थानीय डिवाइस से हटाने के लिए {date} से पहले बनाए गए {count} ऐसेट। फ़ोटो Immich ऐप में देखे जा सकेंगे।", + "cleanup_trash_hint": "स्टोरेज स्पेस पूरी तरह वापस पाने के लिए, सिस्टम गैलरी ऐप खोलें और ट्रैश खाली करें", "clear": "स्पष्ट", "clear_all": "सभी साफ करें", "clear_all_recent_searches": "सभी हालिया खोजें साफ़ करें", @@ -729,8 +782,10 @@ "client_cert_import": "आयात", "client_cert_import_success_msg": "क्लाइंट प्रमाणपत्र आयात किया गया है", "client_cert_invalid_msg": "अमान्य प्रमाणपत्र फ़ाइल या गलत पासवर्ड", + "client_cert_password_message": "इस प्रमाणपत्र के लिए पासवर्ड दर्ज करें", + "client_cert_password_title": "प्रमाणपत्र पासवर्ड", "client_cert_remove_msg": "क्लाइंट प्रमाणपत्र हटा दिया गया है", - "client_cert_subtitle": "केवल PKCS12 (.p12, .pfx) प्रारूप का समर्थन करता है। प्रमाणपत्र आयात/निकालना केवल लॉगिन से पहले ही उपलब्ध है।", + "client_cert_subtitle": "केवल PKCS12 (.p12, .pfx) प्रारूप का समर्थन करता है। प्रमाणपत्र आयात/निकालना केवल लॉगिन से पहले ही उपलब्ध है", "client_cert_title": "SSL क्लाइंट प्रमाणपत्र [प्रायोगिक]", "clockwise": "दक्षिणावर्त", "close": "बंद करें", @@ -738,6 +793,7 @@ "collapse_all": "सभी को संकुचित करें", "color": "रंग", "color_theme": "रंग थीम", + "command": "आदेश", "comment_deleted": "टिप्पणी हटा दी गई", "comment_options": "टिप्पणी विकल्प", "comments_and_likes": "टिप्पणियाँ और पसंद", @@ -782,6 +838,7 @@ "create_album": "एल्बम बनाओ", "create_album_page_untitled": "शीर्षकहीन", "create_api_key": "ऐ.पी.आई. चाभी बनाएं", + "create_first_workflow": "पहला वर्कफ़्लो बनाएं", "create_library": "लाइब्रेरी बनाएं", "create_link": "लिंक बनाएं", "create_link_to_share": "शेयर करने के लिए लिंक बनाएं", @@ -796,14 +853,18 @@ "create_tag": "टैग बनाएँ", "create_tag_description": "एक नया टैग बनाएँ। नेस्टेड टैग के लिए, कृपया फ़ॉरवर्ड स्लैश सहित टैग का पूरा पथ दर्ज करें।", "create_user": "उपयोगकर्ता बनाइये", + "create_workflow": "वर्कफ़्लो बनाएं", "created": "बनाया", "created_at": "बनाया था", "creating_linked_albums": "जुड़े हुए एल्बम बनाए जा रहे हैं..।", "crop": "छाँटें", + "crop_aspect_ratio_free": "स्वतंत्र", + "crop_aspect_ratio_original": "मूल अनुपात", "curated_object_page_title": "चीज़ें", "current_device": "वर्तमान उपकरण", "current_pin_code": "वर्तमान पिन कोड", "current_server_address": "वर्तमान सर्वर पता", + "custom_date": "मनचाही तिथि", "custom_locale": "कस्टम लोकेल", "custom_locale_description": "भाषा और क्षेत्र के आधार पर दिनांक और संख्याएँ प्रारूपित करें", "custom_url": "कस्टम URL", @@ -1178,7 +1239,7 @@ "home_page_delete_remote_err_local": "दूरस्थ चयन को हटाने, छोड़ने में स्थानीय संपत्तियाँ", "home_page_favorite_err_local": "स्थानीय संपत्तियों को अभी तक पसंदीदा नहीं बनाया जा सका, छोड़ा जा रहा है", "home_page_favorite_err_partner": "अब तक पार्टनर एसेट्स को फेवरेट नहीं कर सकते, स्किप कर रहे हैं", - "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": "यदि आप पहली बार इस ऐप का उपयोग कर रहे हैं, तो कृपया एक बैकअप एल्बम चुनें, ताकि टाइमलाइन में फ़ोटो और वीडियो दिखाई दे सकें", "home_page_locked_error_local": "स्थानीय संपत्तियों को लॉक किए गए फ़ोल्डर में नहीं ले जाया जा सकता, छोड़ा जा सकता है", "home_page_locked_error_partner": "साझेदार संपत्तियों को लॉक किए गए फ़ोल्डर में नहीं ले जाया जा सकता, छोड़ें", "home_page_share_err_local": "लोकल एसेट्स को लिंक के जरिए शेयर नहीं कर सकते, स्किप कर रहे हैं", @@ -1447,7 +1508,7 @@ "no_albums_with_name_yet": "ऐसा लगता है कि आपके पास अभी तक इस नाम का कोई एल्बम नहीं है।", "no_albums_yet": "ऐसा लगता है कि आपके पास अभी तक कोई एल्बम नहीं है।", "no_archived_assets_message": "फ़ोटो और वीडियो को अपने फ़ोटो दृश्य से छिपाने के लिए उन्हें संग्रहीत करें", - "no_assets_message": "अपना पहला फोटो अपलोड करने के लिए क्लिक करें", + "no_assets_message": "अपनी पहली फ़ोटो अपलोड करने के लिए क्लिक करें", "no_assets_to_show": "दिखाने के लिए कोई संपत्ति नहीं", "no_cast_devices_found": "कोई कास्ट डिवाइस नहीं मिला", "no_checksum_local": "कोई चेकसम उपलब्ध नहीं है - स्थानीय संपत्तियां प्राप्त नहीं की जा सकतीं", @@ -1474,7 +1535,6 @@ "not_available": "लागू नहीं", "not_in_any_album": "किसी एलबम में नहीं", "not_selected": "चयनित नहीं", - "note_apply_storage_label_to_previously_uploaded assets": "नोट: पहले अपलोड की गई संपत्तियों पर स्टोरेज लेबल लागू करने के लिए, चलाएँ", "notes": "टिप्पणियाँ", "nothing_here_yet": "यहाँ अभी तक कुछ नहीं", "notification_permission_dialog_content": "सूचनाएं सक्षम करने के लिए सेटिंग्स में जाएं और अनुमति दें चुनें।", @@ -1665,11 +1725,11 @@ "readonly_mode_enabled": "केवल-पढ़ने के लिए मोड सक्षम", "ready_for_upload": "अपलोड के लिए तैयार", "reassign": "पुनः असाइन", - "reassigned_assets_to_existing_person": "{count, plural, one {# asset} other {# assets}} को {name, select, null {an existing person} other {{name}}}को फिर से असाइन किया गया", + "reassigned_assets_to_existing_person": "{count, plural, one {# asset} other {# assets}} को {name, select, null {an existing person} other {{name}}}को फिर से असाइन किया गया", "reassigned_assets_to_new_person": "{count, plural, one {# asset} other {# assets}} को एक नए व्यक्ति को फिर से असाइन किया गया", "reassing_hint": "चयनित संपत्तियों को किसी मौजूदा व्यक्ति को सौंपें", "recent": "हाल ही का", - "recent-albums": "हाल के एल्बम", + "recent_albums": "हाल के एल्बम", "recent_searches": "हाल की खोजें", "recently_added": "हाल ही में डाला गया", "recently_added_page_title": "हाल ही में डाला गया", @@ -1874,7 +1934,7 @@ "setting_notifications_notify_failures_grace_period": "बैकग्राउंड बैकअप फेलियर की सूचना दें: {duration}", "setting_notifications_notify_hours": "{count} घंटे", "setting_notifications_notify_immediately": "तुरंत", - "setting_notifications_notify_minutes": "{count} मिनट", + "setting_notifications_notify_minutes": "{count} मिनट", "setting_notifications_notify_never": "कभी नहीं", "setting_notifications_notify_seconds": "{count} सेकंड", "setting_notifications_single_progress_subtitle": "हर एसेट के लिए अपलोड प्रोग्रेस की पूरी जानकारी", @@ -1917,7 +1977,7 @@ "shared_link_custom_url_description": "कस्टम URL से इस शेयर्ड लिंक को एक्सेस करें", "shared_link_edit_description_hint": "शेयर विवरण दर्ज करें", "shared_link_edit_expire_after_option_day": "1 दिन", - "shared_link_edit_expire_after_option_days": "{count} दिन", + "shared_link_edit_expire_after_option_days": "{count} दिन", "shared_link_edit_expire_after_option_hour": "1 घंटा", "shared_link_edit_expire_after_option_hours": "{count} घंटे", "shared_link_edit_expire_after_option_minute": "1 मिनट", @@ -1926,8 +1986,8 @@ "shared_link_edit_expire_after_option_year": "{count} वर्ष", "shared_link_edit_password_hint": "शेयर पासवर्ड दर्ज करें", "shared_link_edit_submit_button": "अपडेट लिंक", - "shared_link_error_server_url_fetch": "सर्वर URL नहीं मिल रहा है", - "shared_link_expires_day": "{count} दिन में समाप्त हो रहा है", + "shared_link_error_server_url_fetch": "सर्वर URL प्राप्त नहीं किया जा सका", + "shared_link_expires_day": "{count} दिन में इसकी वैधता समाप्त हो जाएगी", "shared_link_expires_days": "{count} दिनों में समाप्त हो जाएगा", "shared_link_expires_hour": "{count} घंटे में समाप्त हो जाएगा", "shared_link_expires_hours": "{count} घंटे में समाप्त हो जाएगा", @@ -2052,11 +2112,11 @@ "theme_selection_description": "आपके ब्राउज़र की सिस्टम प्राथमिकता के आधार पर थीम को स्वचालित रूप से प्रकाश या अंधेरे पर सेट करें", "theme_setting_asset_list_storage_indicator_title": "एसेट टाइल्स पर स्टोरेज इंडिकेटर दिखाएं", "theme_setting_asset_list_tiles_per_row_title": "प्रति पंक्ति एसेट की संख्या ({count})", - "theme_setting_colorful_interface_subtitle": "प्राथमिक रंग को पृष्ठभूमि सतहों पर लागू करें", + "theme_setting_colorful_interface_subtitle": "प्राथमिक रंग को पृष्ठभूमि सतहों पर लागू करें।", "theme_setting_colorful_interface_title": "रंगीन इंटरफ़ेस", "theme_setting_image_viewer_quality_subtitle": "डिटेल इमेज व्यूअर की क्वालिटी एडजस्ट करें", "theme_setting_image_viewer_quality_title": "छवि दर्शक गुणवत्ता", - "theme_setting_primary_color_subtitle": "प्राथमिक क्रियाओं और उच्चारणों के लिए एक रंग चुनें", + "theme_setting_primary_color_subtitle": "प्राथमिक क्रियाओं और उच्चारणों के लिए एक रंग चुनें।", "theme_setting_primary_color_title": "प्राथमिक रंग", "theme_setting_system_primary_color_title": "सिस्टम रंग का उपयोग करें", "theme_setting_system_theme_switch": "ऑटोमैटिक (सिस्टम सेटिंग फ़ॉलो करें)", diff --git a/i18n/hr.json b/i18n/hr.json index 83cf002c37..88fa57e230 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -5,6 +5,7 @@ "acknowledge": "Potvrdi", "action": "Akcija", "action_common_update": "Ažuriranje", + "action_description": "Skup radnji koje se izvršavaju nad filtriran", "actions": "Akcije", "active": "Aktivno", "active_count": "Aktivno:{count}", @@ -15,9 +16,14 @@ "add_a_location": "Dodaj lokaciju", "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", + "add_action": "Dodaj akciju", + "add_action_description": "Kliknite za dodavanje radnje koju treba izvršiti", + "add_assets": "Dodaj stavke", "add_birthday": "Dodaj rođendan", "add_endpoint": "Dodaj krajnju točku", "add_exclusion_pattern": "Dodaj uzorak izuzimanja", + "add_filter": "Dodaj filter", + "add_filter_description": "Klikni za dodavanje uvjetnog filtriranja", "add_location": "Dodaj lokaciju", "add_more_users": "Dodaj još korisnika", "add_partner": "Dodaj partnera", @@ -36,6 +42,7 @@ "add_to_shared_album": "Dodaj u dijeljeni album", "add_upload_to_stack": "Dodaj preneseno u skup", "add_url": "Dodaj URL", + "add_workflow_step": "Dodaj korak radnog procesa", "added_to_archive": "Dodano u arhivu", "added_to_favorites": "Dodano u omiljeno", "added_to_favorites_count": "Dodano {count, number} u omiljeno", @@ -97,6 +104,8 @@ "image_preview_description": "Slika srednje veličine s uklonjenim metapodacima, koristi se prilikom pregledavanja jedne stavke i za strojno učenje", "image_preview_quality_description": "Kvaliteta pregleda od 1-100. Više je bolje, ali proizvodi veće datoteke i može smanjiti odziv aplikacije. Postavljanje niske vrijednosti može utjecati na kvalitetu strojnog učenja.", "image_preview_title": "Postavke pregleda", + "image_progressive": "Progresivno", + "image_progressive_description": "Kodiraj JPEG slike progresivno za postupno učitavanje i prikaz. Ovo nema utjecaja na WebP slike.", "image_quality": "Kvaliteta", "image_resolution": "Rezolucija", "image_resolution_description": "Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.", @@ -169,6 +178,11 @@ "machine_learning_ocr_max_resolution": "Maksimalna razlučivost", "machine_learning_ocr_max_resolution_description": "Pregledi preko ove razlučivosti će promijeniti veličinu poštujući omjer slike. Veće vrijednosti su točnije, ali trebaju više vremena za obradu i koriste više memorije.", "machine_learning_ocr_min_detection_score": "Minimalna ocjena prepoznavanja", + "machine_learning_ocr_min_detection_score_description": "Minimalni prag pouzdanosti za detekciju teksta (0–1). Niže vrijednosti otkrit će više teksta, ali mogu dovesti do lažno pozitivnih rezultata.", + "machine_learning_ocr_min_recognition_score": "Minimalni prag prepoznavanja", + "machine_learning_ocr_min_score_recognition_description": "Minimalni prag pouzdanosti za prepoznavanje detektiranog teksta (0–1). Niže vrijednosti prepoznat će više teksta, ali mogu dovesti do lažno pozitivnih rezultata.", + "machine_learning_ocr_model": "Model za prepoznavanje teksta (OCR)", + "machine_learning_ocr_model_description": "Serverski modeli su precizniji od mobilnih modela, ali im treba više vremena za obradu i koriste više memorije.", "machine_learning_settings": "Postavke strojnog učenja", "machine_learning_settings_description": "Upravljajte značajkama i postavkama strojnog učenja", "machine_learning_smart_search": "Pametna pretraga", @@ -176,6 +190,19 @@ "machine_learning_smart_search_enabled": "Omogući pametno pretraživanje", "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametno pretraživanje.", "machine_learning_url_description": "URL poslužitelja strojnog učenja. Ako ste dodali više od jednog URLa, svaki server će biti kontaktiraj jedanput dok jedan ne odgovori uspješno, u redu od prvog do zadnjeg. Serveri koji ne odgovore će privremeno biti ignorirani dok ponovo ne postanu dostupni.", + "maintenance_delete_backup": "Izbriši sigurnosnu kopiju", + "maintenance_delete_backup_description": "Ova datoteka će biti trajno obrisana.", + "maintenance_delete_error": "Brisanje sigurnosne kopije nije uspjelo.", + "maintenance_restore_backup": "Vrati sigurnosnu kopiju", + "maintenance_restore_backup_description": "Immich će biti obrisan i vraćen iz odabrane sigurnosne kopije. Prije nastavka izradit će se nova sigurnosna kopija.", + "maintenance_restore_backup_different_version": "Ova sigurnosna kopija izrađena je s drugom verzijom Immicha!", + "maintenance_restore_backup_unknown_version": "Nije moguće odrediti verziju sigurnosne kopije.", + "maintenance_restore_database_backup": "Vrati sigurnosnu kopiju baze podataka", + "maintenance_restore_database_backup_description": "Vrati bazu podataka na ranije stanje pomoću sigurnosne kopije", + "maintenance_settings": "Održavanje", + "maintenance_settings_description": "Stavi Immich u način održavanja.", + "maintenance_start": "Prebaci se u način održavanja", + "maintenance_start_error": "Neuspjelo pokretanje načina održavanja.", "manage_concurrency": "Upravljanje Istovremenošću", "manage_log_settings": "Upravljanje postavkama zapisivanje", "map_dark_style": "Tamni stil", @@ -1432,7 +1459,6 @@ "not_available": "N/A", "not_in_any_album": "Ni u jednom albumu", "not_selected": "Nije odabrano", - "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primijenili oznaku pohrane na prethodno prenesene stavke, pokrenite", "notes": "Bilješke", "nothing_here_yet": "Ovdje još nema ničega", "notification_permission_dialog_content": "Da biste omogućili obavijesti, idite u Postavke i odaberite dopusti.", @@ -1619,7 +1645,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# stavka ponovno dodijeljena} few {# stavke ponovno dodijeljene} other {# stavki ponovno dodijeljeno}} novoj osobi", "reassing_hint": "Dodijelite odabrane stavke postojećoj osobi", "recent": "Nedavno", - "recent-albums": "Nedavni albumi", + "recent_albums": "Nedavni albumi", "recent_searches": "Nedavne pretrage", "recently_added": "Nedavno dodano", "recently_added_page_title": "Nedavno dodano", diff --git a/i18n/hu.json b/i18n/hu.json index 232d492dd7..c2f3362e18 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -1604,7 +1604,6 @@ "not_available": "N/A", "not_in_any_album": "Nincs albumban", "not_selected": "Nincs kiválasztva", - "note_apply_storage_label_to_previously_uploaded assets": "Megjegyzés: a korábban feltöltött elemek tárhely címkézéséhez futtasd a(z)", "notes": "Megjegyzések", "nothing_here_yet": "Még semmi sincs itt", "notification_permission_dialog_content": "Az értesítések bekapcsolásához a Beállítások menüben válaszd ki az Engedélyezés-t.", @@ -1806,7 +1805,7 @@ "reassigned_assets_to_new_person": "{count, plural, other {# elem}} hozzárendelve egy új személyhez", "reassing_hint": "Kijelölt elemek létező személyhez rendelése", "recent": "Friss", - "recent-albums": "Legutóbbi albumok", + "recent_albums": "Legutóbbi albumok", "recent_searches": "Legutóbbi keresések", "recently_added": "Nemrég hozzáadott", "recently_added_page_title": "Nemrég hozzáadott", diff --git a/i18n/id.json b/i18n/id.json index 6f00e98867..acde13c7d8 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -198,12 +198,12 @@ "maintenance_restore_backup_different_version": "Cadangan ini dibuat dengan versi Immich yang berbeda!", "maintenance_restore_backup_unknown_version": "Tidak dapat menentukan versi candangan.", "maintenance_restore_database_backup": "Mengembalikan cadangan database", - "maintenance_restore_database_backup_description": "Kembalikan ke keadaan database sebelumnya menggunakan sebuah file cadangan", + "maintenance_restore_database_backup_description": "Kembalikan ke status basis data yang lebih awal menggunakan berkas cadangan", "maintenance_settings": "Pemeliharaan", "maintenance_settings_description": "Setel mode pemeliharaan Immich.", "maintenance_start": "Pindah ke mode pemeliharaan", "maintenance_start_error": "Gagal memulai mode pemeliharaan.", - "maintenance_upload_backup": "Unggah file candangan database", + "maintenance_upload_backup": "Unggah berkas cadangan basis data", "maintenance_upload_backup_error": "Tidak dapat mengunggah cadangan, apakah ini sebuah file .sql/.sql.gz?", "manage_concurrency": "Kelola Konkurensi", "manage_concurrency_description": "Pindah ke halaman tugas untuk mengelola konkurensi tugas", @@ -765,7 +765,7 @@ "cleanup_deleting": "Memindahkan ke tempat sampah...", "cleanup_found_assets": "Menemukan {count} aset cadangan", "cleanup_found_assets_with_size": "Menemukan {count} aset cadangan ({size})", - "cleanup_icloud_shared_albums_excluded": "iCloud Shared Albums dikecualikan dari pemindaian", + "cleanup_icloud_shared_albums_excluded": "Album Bersama iCloud dikecualikan dari pemindaian", "cleanup_no_assets_found": "Tidak ada aset yang ditemukan dengan kriteria diatas. Fitur membebaskan ruang hanya dapat menghapus aset yang dicadangkan ke server", "cleanup_preview_title": "Aset yang akan dihapus ({count})", "cleanup_step3_description": "Pindah untuk cadangan aset yang sesuai dengan tanggal mu dan simpan pengaturan.", @@ -782,6 +782,8 @@ "client_cert_import": "Impor", "client_cert_import_success_msg": "Sertifikat klien telah diimpor", "client_cert_invalid_msg": "File sertifikat tidak valid atau kata sandi salah", + "client_cert_password_message": "Masukkan kata sandi untuk sertifikat ini", + "client_cert_password_title": "Kata Sandi Sertifikat", "client_cert_remove_msg": "Sertifikat klien dihapus", "client_cert_subtitle": "Hanya mendukung format PKCS12 (.p12, .pfx). Impor/hapus sertifikat hanya tersedia sebelum login", "client_cert_title": "Sertifikat SSL klien [EKSPERIMENTAL]", @@ -867,7 +869,9 @@ "custom_locale": "Lokal Khusus", "custom_locale_description": "Format tanggal dan angka berdasarkan bahasa dan wilayah", "custom_url": "URL Kustom", - "cutoff_date_description": "Simpan foto dari…", + "cutoff_date_description": "Simpan foto dari … terakhir", + "cutoff_day": "{count, plural, one {hari} other {hari}}", + "cutoff_year": "{count, plural, one {tahun} other {tahun}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM yyyy", "dark": "Gelap", @@ -993,6 +997,11 @@ "editor_close_without_save_prompt": "Perubahan tidak akan di simpan", "editor_close_without_save_title": "Tutup editor?", "editor_confirm_reset_all_changes": "Apakah anda yakin mau mengatur ulang semua perubahan?", + "editor_discard_edits_confirm": "Buang suntingan", + "editor_discard_edits_prompt": "Anda memiliki suntingan yang belum disimpan. Apakah Anda yakin ingin membuangnya?", + "editor_discard_edits_title": "Buang suntingan?", + "editor_edits_applied_error": "Gagal menerapkan suntingan", + "editor_edits_applied_success": "Suntingan berhasil diterapkan", "editor_flip_horizontal": "Balik horizontal", "editor_flip_vertical": "Balik vertikal", "editor_orientation": "Orientasi", @@ -1194,6 +1203,8 @@ "features_in_development": "Fitur dalam Pengembangan", "features_setting_description": "Kelola fitur aplikasi", "file_name_or_extension": "Nama berkas atau ekstensi", + "file_name_text": "Nama berkas", + "file_name_with_value": "Nama berkas: {file_name}", "file_size": "Ukuran berkas", "filename": "Nama berkas", "filetype": "Jenis berkas", @@ -1212,6 +1223,8 @@ "forgot_pin_code_question": "Lupa PIN?", "forward": "Maju", "free_up_space": "Bebaskan ruang", + "free_up_space_description": "Pindahkan foto dan video yang dicadangkan ke tempat sampah perangkat Anda untuk mengosongkan ruang. Salinan Anda di server tetap aman.", + "free_up_space_settings_subtitle": "Kosongkan penyimpanan perangkat", "full_path": "Jalur lengkap: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Fitur ini memuat sumber daya eksternal dari Google agar dapat berfungsi.", @@ -1327,8 +1340,15 @@ "json_editor": "Editor JSON", "json_error": "Kesalahan JSON", "keep": "Simpan", + "keep_albums": "Simpan album", + "keep_albums_count": "Menyimpan {count} {count, plural, one {album} other {album}}", "keep_all": "Simpan Semua", + "keep_description": "Pilih apa yang tetap berada di perangkat Anda saat mengosongkan ruang.", + "keep_favorites": "Simpan favorit", + "keep_on_device": "Simpan di perangkat", + "keep_on_device_hint": "Pilih item yang akan disimpan di perangkat ini", "keep_this_delete_others": "Pertahankan ini, hapus lainnya", + "keeping": "Menyimpan: {items}", "kept_this_deleted_others": "Aset ini dipertahankan dan {count, plural, one {# asset} other {# assets}} dihapus", "keyboard_shortcuts": "Pintasan papan ketik", "language": "Bahasa", @@ -1422,10 +1442,28 @@ "loop_videos_description": "Aktifkan untuk mengulangi video secara otomatis dalam penampil detail.", "main_branch_warning": "Anda menggunakan versi pengembangan; kami sangat menyarankan menggunakan versi rilis!", "main_menu": "Menu utama", + "maintenance_action_restore": "Memulihkan Basis Data", "maintenance_description": "Immich telah ditempatkan di mode pemeliharaan.", "maintenance_end": "Akhiri mode pemeliharaan", "maintenance_end_error": "Gagal mengakhiri mode pemeliharaan.", "maintenance_logged_in_as": "Saat ini masuk sebagai {user}", + "maintenance_restore_from_backup": "Pulihkan Dari Cadangan", + "maintenance_restore_library": "Pulihkan Pustaka Anda", + "maintenance_restore_library_confirm": "Jika ini terlihat benar, lanjutkan untuk memulihkan cadangan!", + "maintenance_restore_library_description": "Memulihkan Basis Data", + "maintenance_restore_library_folder_has_files": "{folder} memiliki {count} folder", + "maintenance_restore_library_folder_no_files": "{folder} kehilangan berkas!", + "maintenance_restore_library_folder_pass": "dapat dibaca dan dapat ditulis", + "maintenance_restore_library_folder_read_fail": "tidak dapat dibaca", + "maintenance_restore_library_folder_write_fail": "tidak dapat ditulis", + "maintenance_restore_library_hint_missing_files": "Anda mungkin kehilangan berkas penting", + "maintenance_restore_library_hint_regenerate_later": "Anda dapat membuat ulang ini nanti di pengaturan", + "maintenance_restore_library_hint_storage_template_missing_files": "Menggunakan templat penyimpanan? Anda mungkin kehilangan berkas", + "maintenance_restore_library_loading": "Memuat pemeriksaan integritas dan heuristik…", + "maintenance_task_backup": "Membuat cadangan dari basis data yang ada…", + "maintenance_task_migrations": "Menjalankan migrasi basis data…", + "maintenance_task_restore": "Memulihkan cadangan yang dipilih…", + "maintenance_task_rollback": "Pemulihan gagal, mengembalikan ke titik pemulihan…", "maintenance_title": "Tidak Tersedia untuk Sementara", "make": "Merek", "manage_geolocation": "Atur lokasi", @@ -1487,6 +1525,8 @@ "minimize": "Kecilkan", "minute": "Menit", "minutes": "Menit", + "mirror_horizontal": "Horisontal", + "mirror_vertical": "Vertikal", "missing": "Hilang", "mobile_app": "Aplikasi Seluler", "mobile_app_download_onboarding_note": "Unduh aplikasi seluler pendamping dengan menggunakan opsi berikut", @@ -1498,6 +1538,7 @@ "move_down": "Pindah ke bawah", "move_off_locked_folder": "Pindahkan dari folder terkunci", "move_to": "Pindah ke", + "move_to_device_trash": "Pindahkan ke tempat sampah perangkat", "move_to_lock_folder_action_prompt": "{count} ditambahkan ke folder terkunci", "move_to_locked_folder": "Pindahkan dari folder terkunci", "move_to_locked_folder_confirmation": "Foto dan video ini akan dihapus dari semua album, dan hanya dapat dilihat dari folder terkunci", @@ -1537,11 +1578,12 @@ "next_memory": "Kenangan berikutnya", "no": "Tidak", "no_actions_added": "Belum ada aksi yang ditambahkan", + "no_albums_found": "Tidak ada album yang ditemukan", "no_albums_message": "Buat album untuk mengelola foto dan video Anda", "no_albums_with_name_yet": "Sepertinya Anda belum memiliki album apa pun dengan nama ini.", "no_albums_yet": "Sepertinya Anda belum memiliki album apa pun.", "no_archived_assets_message": "Arsipkan foto dan video untuk menyembunyikannya dari tampilan Foto", - "no_assets_message": "KLIK UNTUK MENGUNGGAH FOTO PERTAMA ANDA", + "no_assets_message": "Klik untuk mengunggah foto pertama Anda", "no_assets_to_show": "Tidak ada aset", "no_cast_devices_found": "Tidak ada perangkat cast yang ditemukan", "no_checksum_local": "Tidak ada checksum yang tersedia - tidak dapat mengambil aset lokal", @@ -1566,11 +1608,11 @@ "no_results_description": "Coba sinonim atau kata kunci yang lebih umum", "no_shared_albums_message": "Buat sebuah album untuk membagikan foto dan video dengan orang-orang dalam jaringan Anda", "no_uploads_in_progress": "Tidak ada unggahan yang sedang berlangsung", + "none": "Tidak ada", "not_allowed": "Tidak diperbolehkan", "not_available": "T/T", "not_in_any_album": "Tidak ada dalam album apa pun", "not_selected": "Belum dipilih", - "note_apply_storage_label_to_previously_uploaded assets": "Catatan: Untuk menerapkan Label Penyimpanan pada aset yang sebelumnya telah diunggah, jalankan", "notes": "Catatan", "nothing_here_yet": "Masih kosong", "notification_permission_dialog_content": "Untuk mengaktifkan notifikasi, buka Pengaturan lalu berikan izin.", @@ -1680,6 +1722,7 @@ "photos_and_videos": "Foto & Video", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foto}}", "photos_from_previous_years": "Foto dari tahun lalu", + "photos_only": "Hanya foto", "pick_a_location": "Pilih lokasi", "pick_custom_range": "Rentang kustom", "pick_date_range": "Pilih rentang tanggal", @@ -1771,7 +1814,7 @@ "reassigned_assets_to_new_person": "Menetapkan ulang {count, plural, one {# aset} other {# aset}} kepada orang baru", "reassing_hint": "Tetapkan aset yang dipilih ke orang yang sudah ada", "recent": "Terkini", - "recent-albums": "Album terkini", + "recent_albums": "Album terkini", "recent_searches": "Pencarian terkini", "recently_added": "Barusaja ditambahkan", "recently_added_page_title": "Baru Ditambahkan", @@ -1860,9 +1903,11 @@ "saved_settings": "Pengaturan disimpan", "say_something": "Ucapkan sesuatu", "scaffold_body_error_occurred": "Terjadi kesalahan", + "scan": "Pindai", "scan_all_libraries": "Pindai Semua Pustaka", "scan_library": "Pindai", "scan_settings": "Pengaturan Pemindaian", + "scanning": "Memindai", "scanning_for_album": "Memindai album...", "search": "Cari", "search_albums": "Cari album", @@ -1892,6 +1937,7 @@ "search_filter_media_type_title": "Pilih jenis media", "search_filter_ocr": "Cari dengan OCR", "search_filter_people_title": "Pilih orang", + "search_filter_star_rating": "Peringkat Bintang", "search_for": "Cari", "search_for_existing_person": "Cari orang yang sudah ada", "search_no_more_result": "Tidak ada hasil lagi", @@ -1934,6 +1980,7 @@ "select_all_in": "Pilih semua di {group}", "select_avatar_color": "Pilih warna avatar", "select_count": "{count, plural, one {Pilih #} other {Pilih #}}", + "select_cutoff_date": "Pilih tanggal batas", "select_face": "Pilih wajah", "select_featured_photo": "Pilih foto terfitur", "select_from_computer": "Pilih dari komputer", @@ -2095,6 +2142,8 @@ "skip_to_folders": "Lewati ke berkas", "skip_to_tags": "Lewati ke tag", "slideshow": "Salindia", + "slideshow_repeat": "Ulangi slideshow", + "slideshow_repeat_description": "Ulangi dari awal saat slideshow berakhir", "slideshow_settings": "Pengaturan salindia", "sort_albums_by": "Urutkan album berdasarkan...", "sort_created": "Tanggal dibuat", @@ -2171,6 +2220,7 @@ "theme_setting_theme_subtitle": "Pilih setelan tema aplikasi", "theme_setting_three_stage_loading_subtitle": "Pemuatan tiga tahap dapat meningkatkan performa pemuatan, namun akan menyebabkan beban jaringan meningkat secara signifikan", "theme_setting_three_stage_loading_title": "Aktifkan pemuatan tiga tahap", + "then": "Lalu", "they_will_be_merged_together": "Mereka akan digabungkan bersama", "third_party_resources": "Sumber Daya Pihak Ketiga", "time": "Waktu", @@ -2226,6 +2276,7 @@ "unhide_person": "Munculkan orang", "unknown": "Tidak diketahui", "unknown_country": "Negara Tidak Diketahui", + "unknown_date": "Tanggal tidak diketahui", "unknown_year": "Tahun Tidak Diketahui", "unlimited": "Tidak terbatas", "unlink_motion_video": "Membatalkan tautan video gerak", @@ -2254,6 +2305,7 @@ "upload_details": "Detil unggahan", "upload_dialog_info": "Apakah akan mencadangkan aset terpilih ke server?", "upload_dialog_title": "Unggah Aset", + "upload_error_with_count": "Kesalahan unggah untuk {count, plural, one {# aset} other {# aset}}", "upload_errors": "Unggahan selesai dengan {count, plural, one {# eror} other {# eror}}, muat ulang laman untuk melihat aset terunggah baru.", "upload_finished": "Unggahan berhasil", "upload_progress": "Tersisa {remaining, number} - Di proses {processed, number}/{total, number}", @@ -2301,6 +2353,7 @@ "video_hover_setting_description": "Putar gambar kecil video ketika tetikus berada di atas item. Bahkan saat dinonaktifkan, pemutaran dapat dimulai dengan mengambang di atas ikon putar.", "videos": "Video", "videos_count": "{count, plural, one {# Video} other {# Video}}", + "videos_only": "Hanya video", "view": "Tampilkan", "view_album": "Tampilkan Album", "view_all": "Tampilkan Semua", diff --git a/i18n/it.json b/i18n/it.json index 50d0632f3d..e9cc787096 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -311,7 +311,7 @@ "search_jobs": "Cerca Attività…", "send_welcome_email": "Invia email di benvenuto", "server_external_domain_settings": "Dominio esterno", - "server_external_domain_settings_description": "Dominio per link condivisi pubblicamente, incluso http(s)://", + "server_external_domain_settings_description": "Dominio utilizzato per i link esterni", "server_public_users": "Utenti Pubblici", "server_public_users_description": "Tutti gli utenti (nome ed e-mail) sono elencati quando si aggiunge un utente agli album condivisi. Quando disabilitato, l'elenco degli utenti sarà disponibile solo per gli utenti amministratori.", "server_settings": "Impostazioni Server", @@ -782,6 +782,8 @@ "client_cert_import": "Importa", "client_cert_import_success_msg": "Certificato client importato", "client_cert_invalid_msg": "File certificato invalido o password errata", + "client_cert_password_message": "Inserisci la password per questo certificato", + "client_cert_password_title": "Password del certificato", "client_cert_remove_msg": "Certificato client rimosso", "client_cert_subtitle": "Supporta solo il formato PKCS12 (.p12, .pfx). L'importazione/rimozione del certificato è disponibile solo prima del login", "client_cert_title": "Certificato Client SSL [SPERIMENTALE]", @@ -792,6 +794,11 @@ "color": "Colore", "color_theme": "Colore Tema", "command": "Comando", + "command_palette_prompt": "Trova rapidamente pagine, azioni o comandi", + "command_palette_to_close": "per chiudere", + "command_palette_to_navigate": "per entrare", + "command_palette_to_select": "per selezionare", + "command_palette_to_show_all": "per mostrare tutto", "comment_deleted": "Commento eliminato", "comment_options": "Opzioni per i commenti", "comments_and_likes": "Commenti & mi piace", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "Le modifiche non verranno salvate", "editor_close_without_save_title": "Vuoi chiudere l'editor?", "editor_confirm_reset_all_changes": "Sicuro di voler resettare tutte le modifiche?", + "editor_discard_edits_confirm": "Ignora modifiche", + "editor_discard_edits_prompt": "Hai delle modifiche non salvate. Vuoi davvero eliminarle?", + "editor_discard_edits_title": "Ignorare le modifiche?", + "editor_edits_applied_error": "Impossibile applicare le modifiche", + "editor_edits_applied_success": "Modifiche applicate con successo", "editor_flip_horizontal": "Capovolgi in orizzontale", "editor_flip_vertical": "Capovolgi in verticale", "editor_orientation": "Orientamento", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONE", "exif_bottom_sheet_person_add_person": "Aggiungi nome", "exit_slideshow": "Esci dalla presentazione", + "expand": "Espandi", "expand_all": "Espandi tutto", "experimental_settings_new_asset_list_subtitle": "Lavori in corso", "experimental_settings_new_asset_list_title": "Attiva griglia foto sperimentale", @@ -1196,6 +1209,8 @@ "features_in_development": "Funzionalità in fase di sviluppo", "features_setting_description": "Gestisci le funzionalità dell'app", "file_name_or_extension": "Nome file o estensione", + "file_name_text": "Nome del file", + "file_name_with_value": "Nome del file: {file_name}", "file_size": "Dimensione del file", "filename": "Nome file", "filetype": "Tipo file", @@ -1604,7 +1619,6 @@ "not_available": "N/A", "not_in_any_album": "In nessun album", "not_selected": "Non selezionato", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Per aggiungere l'etichetta dell'archiviazione alle risorse caricate in precedenza, esegui", "notes": "Note", "nothing_here_yet": "Ancora nulla qui", "notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi.", @@ -1634,6 +1648,7 @@ "online": "Online", "only_favorites": "Solo preferiti", "open": "Apri", + "open_calendar": "Apri il calendario", "open_in_map_view": "Apri nella visualizzazione mappa", "open_in_openstreetmap": "Apri su OpenStreetMap", "open_the_search_filters": "Apri filtri di ricerca", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {Riassegnata # risorsa} other {Riassegnate # risorse}} ad una nuova persona", "reassing_hint": "Assegna le risorse selezionate ad una persona esistente", "recent": "Recenti", - "recent-albums": "Album recenti", + "recent_albums": "Album recenti", "recent_searches": "Ricerche recenti", "recently_added": "Aggiunti recentemente", "recently_added_page_title": "Aggiunti di recente", @@ -2175,6 +2190,7 @@ "support": "Supporto", "support_and_feedback": "Supporto & Feedback", "support_third_party_description": "La tua installazione di Immich è stata costruita da terze parti. I problemi che riscontri potrebbero essere causati da altri pacchetti, quindi ti preghiamo di sollevare il problema in prima istanza utilizzando i link sottostanti.", + "supporter": "Sostenitore", "swap_merge_direction": "Scambia direzione di unione", "sync": "Sincronizza", "sync_albums": "Sincronizza album", diff --git a/i18n/ja.json b/i18n/ja.json index b89e335004..218e615ac1 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -782,6 +782,8 @@ "client_cert_import": "インポート", "client_cert_import_success_msg": "クライアント証明書が導入されました", "client_cert_invalid_msg": "パスワードが間違っているか証明書が無効です", + "client_cert_password_message": "この証明書のパスワードを入力してください", + "client_cert_password_title": "証明書パスワード", "client_cert_remove_msg": "クライアント証明書が削除されました", "client_cert_subtitle": "PKCS12 (.p12 .pfx) フォーマットのみ対応しています。証明書の導入や削除はログイン前のみ行えます", "client_cert_title": "SSLクライアント証明書 [実験的]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "変更は破棄されます", "editor_close_without_save_title": "編集画面を閉じますか?", "editor_confirm_reset_all_changes": "本当に全ての変更をリセットしますか?", + "editor_discard_edits_confirm": "編集を破棄する", + "editor_discard_edits_prompt": "編集内容が保存されていません。破棄しますか?", + "editor_discard_edits_title": "編集を破棄しますか?", + "editor_edits_applied_error": "編集の適用に失敗しました", + "editor_edits_applied_success": "編集が正常に反映されました", "editor_flip_horizontal": "水平方向に反転", "editor_flip_vertical": "垂直に反転", "editor_orientation": "向き", @@ -1196,6 +1203,8 @@ "features_in_development": "開発中の機能", "features_setting_description": "アプリの機能を管理する", "file_name_or_extension": "ファイル名または拡張子", + "file_name_text": "ファイル名", + "file_name_with_value": "ファイル名: {file_name}", "file_size": "ファイルサイズ", "filename": "ファイル名", "filetype": "ファイルタイプ", @@ -1604,7 +1613,6 @@ "not_available": "適用なし", "not_in_any_album": "どのアルバムにも入っていない", "not_selected": "選択なし", - "note_apply_storage_label_to_previously_uploaded assets": "注意: 以前にアップロードしたアセットにストレージラベルを適用するには以下を実行してください", "notes": "注意", "nothing_here_yet": "まだ何も無いようです", "notification_permission_dialog_content": "通知を許可するには設定を開いてオンにしてください", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {#個} other {#個}}の写真/動画を新しい人物に割り当てました", "reassing_hint": "選択された写真/動画を既存の人物に割り当て", "recent": "最近", - "recent-albums": "最近のアルバム", + "recent_albums": "最近のアルバム", "recent_searches": "最近の検索", "recently_added": "最近追加された項目", "recently_added_page_title": "最近", diff --git a/i18n/ka.json b/i18n/ka.json index f8d98ed25c..f386a1e357 100644 --- a/i18n/ka.json +++ b/i18n/ka.json @@ -5,6 +5,7 @@ "acknowledge": "მიღება", "action": "ქმედება", "action_common_update": "განაახლე", + "action_description": "მოქმედებები გაფილტრულ რესურსებზე", "actions": "ქმედებები", "active": "აქტიური", "active_count": "aქტიური: {count}", @@ -16,11 +17,13 @@ "add_a_name": "დაამატე სახელი", "add_a_title": "დაასათაურე", "add_action": "დაამატე მოქმედება", + "add_action_description": "დააჭირე რომ დაამატო მოქმედება", "add_assets": "რესურსის ატვირთვა", "add_birthday": "დაბადების დღის დამატება", "add_endpoint": "ბოლოწერტილის დამატება", "add_exclusion_pattern": "დაამატე გამონაკლისი ნიმუში", "add_filter": "დაამატე ფილტრი", + "add_filter_description": "დააჭირე ფილტრის დასამატებლად", "add_location": "დაამატე ადგილი", "add_more_users": "დაამატე მომხმარებლები", "add_partner": "დაამატე პარტნიორი", @@ -31,6 +34,7 @@ "add_to_album": "დაამატე ალბომში", "add_to_album_bottom_sheet_added": "დამატებულია {album}-ში", "add_to_album_bottom_sheet_already_exists": "{album}-ში უკვე არსებობს", + "add_to_album_bottom_sheet_some_local_assets": "ზოგიერთი ლოკალური რესურსი ვერ დაემატა ალბომში", "add_to_albums": "დაამატე ალბომებში", "add_to_albums_count": "დაამატე ალბომში ({count})", "add_to_bottom_bar": "დამატება სად", diff --git a/i18n/kn.json b/i18n/kn.json index ec7c174e69..f6dde7bf8f 100644 --- a/i18n/kn.json +++ b/i18n/kn.json @@ -18,6 +18,7 @@ "add_a_title": "ಶೀರ್ಷಿಕೆಯನ್ನು ಸೇರಿಸಿ", "add_action": "ಕ್ರಿಯೆಯನ್ನು ಸೇರಿಸಿ", "add_action_description": "ನಿರ್ವಹಿಸಲು ಕ್ರಿಯೆಯನ್ನು ಸೇರಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ", + "add_assets": "ಆಸ್ತಿ ಸೇರಿಸಿ", "add_birthday": "ಜನ್ಮದಿನ ಸೇರಿಸಿ", "add_endpoint": "ಎಂಡ್‌ಪಾಯಿಂಟ್ ಸೇರಿಸಿ", "add_exclusion_pattern": "ಹೊರಗಿಡುವಿಕೆ ಮಾದರಿಯನ್ನು ಸೇರಿಸಿ", @@ -31,7 +32,7 @@ "add_tag": "ಟ್ಯಾಗ್ ಸೇರಿಸಿ", "add_to": "ಸೇರಿಸಿ…", "add_to_album": "ಆಲ್ಬಮ್‌ಗೆ ಸೇರಿಸಿ", - "add_to_album_bottom_sheet_added": "{album}ಗೆ ಸೇರಿಸಿದೆ", + "add_to_album_bottom_sheet_added": "{album} ಗೆ ಸೇರಿಸಲಾಗಿದೆ", "add_to_album_bottom_sheet_already_exists": "ಈಗಾಗಲೇ {album} ನಲ್ಲಿದೆ", "add_to_album_bottom_sheet_some_local_assets": "ಕೆಲವು ಸ್ಥಳೀಯ ಸ್ವತ್ತುಗಳನ್ನು ಆಲ್ಬಮ್‌ಗೆ ಸೇರಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ", "add_to_album_toggle": "{album}ಗಾಗಿ ಆಯ್ಕೆಯನ್ನು ಟಾಗಲ್ ಮಾಡಿ", @@ -47,9 +48,11 @@ "added_to_favorites_count": "{count, number} ಮೆಚ್ಚಿನವುಗಳಿಗೆ ಸೇರಿಸಲಾಗಿದೆ", "admin": { "admin_user": "ನಿರ್ವಾಹಕ ಬಳಕೆದಾರ", + "asset_offline_description": "ಈ ಬಾಹ್ಯ ಲೈಬ್ರರಿ ಸ್ವತ್ತು ಇನ್ನು ಮುಂದೆ ಡಿಸ್ಕ್‌ನಲ್ಲಿ ಕಂಡುಬರುವುದಿಲ್ಲ ಮತ್ತು ಅದನ್ನು ಅನುಪಯುಕ್ತಕ್ಕೆ ಸರಿಸಲಾಗಿದೆ. ಫೈಲ್ ಅನ್ನು ಲೈಬ್ರರಿಯೊಳಗೆ ಸರಿಸಲಾಗಿದೆಯಾದರೆ, ಹೊಸ ಅನುಗುಣವಾದ ಸ್ವತ್ತಿಗಾಗಿ ನಿಮ್ಮ ಟೈಮ್‌ಲೈನ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ. ಈ ಸ್ವತ್ತನ್ನು ಪುನಃಸ್ಥಾಪಿಸಲು, ದಯವಿಟ್ಟು ಕೆಳಗಿನ ಫೈಲ್ ಮಾರ್ಗವನ್ನು ಇಮ್ಮಿಚ್ ಪ್ರವೇಶಿಸಬಹುದೆಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ ಮತ್ತು ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿ.", "authentication_settings": "ದೃಢೀಕರಣ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "authentication_settings_description": "ಪಾಸ್‌ವರ್ಡ್, ಒಔತ್ ಮತ್ತು ಇತರ ದೃಢೀಕರಣ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ", "authentication_settings_disable_all": "ನೀವು ಎಲ್ಲಾ ಲಾಗಿನ್ ವಿಧಾನಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ? ಲಾಗಿನ್ ಅನ್ನು ಸಂಪೂರ್ಣವಾಗಿ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗುತ್ತದೆ.", + "authentication_settings_reenable": "ಮರು-ಸಕ್ರಿಯಗೊಳಿಸಲು, ಸರ್ವರ್ ಕಮಾಂಡ್ ಅನ್ನು ಬಳಸಿ.", "background_task_job": "ಹಿನ್ನೆಲೆ ಕಾರ್ಯಗಳು", "backup_database": "ಡೇಟಾಬೇಸ್ ಡಂಪ್ ರಚಿಸಿ", "backup_database_enable_description": "ಡೇಟಾಬೇಸ್ ಡಂಪ್‌ಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", @@ -73,6 +76,7 @@ "create_job": "ಉದ್ಯೋಗ ರಚಿಸಿ", "cron_expression_presets": "ಕ್ರಾನ್ ಅಭಿವ್ಯಕ್ತಿ ಪೂರ್ವನಿಗದಿಗಳು", "disable_login": "ಲಾಗಿನ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ", + "duplicate_detection_job_description": "ಒಂದೇ ರೀತಿಯ ಚಿತ್ರಗಳನ್ನು ಪತ್ತೆಹಚ್ಚಲು ಸ್ವತ್ತುಗಳ ಮೇಲೆ ಯಂತ್ರ ಕಲಿಕೆಯನ್ನು ರನ್ ಮಾಡಿ. ಸ್ಮಾರ್ಟ್ ಹುಡುಕಾಟವನ್ನು ಅವಲಂಬಿಸಿದೆ", "export_config_as_json_description": "ಪ್ರಸ್ತುತ ಸಿಸ್ಟಮ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು JSON ಫೈಲ್ ಆಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ", "external_libraries_page_description": "ನಿರ್ವಾಹಕ ಬಾಹ್ಯ ಗ್ರಂಥಾಲಯ ಪುಟ", "face_detection": "ಮುಖ ಪತ್ತೆ", @@ -82,16 +86,23 @@ "image_format_description": "WebP, JPEG ಗಿಂತ ಚಿಕ್ಕ ಫೈಲ್‌ಗಳನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ, ಆದರೆ ಎನ್‌ಕೋಡ್ ಮಾಡಲು ನಿಧಾನವಾಗಿರುತ್ತದೆ.", "image_fullsize_description": "ಝೂಮ್ ಇನ್ ಮಾಡಿದಾಗ ಬಳಸಲಾದ, ಸ್ಟ್ರಿಪ್ಡ್ ಮೆಟಾಡೇಟಾ ಹೊಂದಿರುವ ಪೂರ್ಣ-ಗಾತ್ರದ ಚಿತ್ರ", "image_fullsize_enabled": "ಪೂರ್ಣ-ಗಾತ್ರದ ಚಿತ್ರ ರಚನೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", + "image_fullsize_enabled_description": "ವೆಬ್ ಸ್ನೇಹಿಯಲ್ಲದ ಸ್ವರೂಪಗಳಿಗಾಗಿ ಪೂರ್ಣ-ಗಾತ್ರದ ಚಿತ್ರವನ್ನು ರಚಿಸಿ. \"ಎಂಬೆಡೆಡ್ ಪೂರ್ವವೀಕ್ಷಣೆಯನ್ನು ಆದ್ಯತೆ ನೀಡಿ\" ಸಕ್ರಿಯಗೊಳಿಸಿದಾಗ, ಎಂಬೆಡೆಡ್ ಪೂರ್ವವೀಕ್ಷಣೆಗಳನ್ನು ಪರಿವರ್ತನೆ ಇಲ್ಲದೆ ನೇರವಾಗಿ ಬಳಸಲಾಗುತ್ತದೆ. JPEG ನಂತಹ ವೆಬ್ ಸ್ನೇಹಿ ಸ್ವರೂಪಗಳ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರುವುದಿಲ್ಲ.", "image_fullsize_quality_description": "1-100 ರವರೆಗಿನ ಪೂರ್ಣ-ಗಾತ್ರದ ಚಿತ್ರದ ಗುಣಮಟ್ಟ. ಹೆಚ್ಚಿನದು ಉತ್ತಮ, ಆದರೆ ದೊಡ್ಡ ಫೈಲ್‌ಗಳನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ.", "image_fullsize_title": "ಪೂರ್ಣ-ಗಾತ್ರದ ಚಿತ್ರ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "image_prefer_embedded_preview": "ಎಂಬೆಡ್ ಮಾಡಿದ ಪೂರ್ವವೀಕ್ಷಣೆಗೆ ಆದ್ಯತೆ ನೀಡಿ", "image_prefer_wide_gamut": "ವಿಶಾಲ ವ್ಯಾಪ್ತಿಗೆ ಆದ್ಯತೆ ನೀಡಿ", + "image_prefer_wide_gamut_setting_description": "ಥಂಬ್‌ನೇಲ್‌ಗಳಿಗಾಗಿ ಡಿಸ್ಪ್ಲೇ P3 ಬಳಸಿ. ಇದು ವಿಶಾಲವಾದ ಬಣ್ಣಗಳ ಸ್ಥಳಗಳನ್ನು ಹೊಂದಿರುವ ಚಿತ್ರಗಳ ಕಂಪನವನ್ನು ಉತ್ತಮವಾಗಿ ಸಂರಕ್ಷಿಸುತ್ತದೆ, ಆದರೆ ಹಳೆಯ ಬ್ರೌಸರ್ ಆವೃತ್ತಿಯನ್ನು ಹೊಂದಿರುವ ಹಳೆಯ ಸಾಧನಗಳಲ್ಲಿ ಚಿತ್ರಗಳು ವಿಭಿನ್ನವಾಗಿ ಗೋಚರಿಸಬಹುದು. ಬಣ್ಣ ಬದಲಾವಣೆಗಳನ್ನು ತಪ್ಪಿಸಲು sRGB ಚಿತ್ರಗಳನ್ನು sRGB ಆಗಿ ಇರಿಸಲಾಗುತ್ತದೆ.", + "image_preview_description": "ಒಂದೇ ಸ್ವತ್ತನ್ನು ವೀಕ್ಷಿಸುವಾಗ ಮತ್ತು ಯಂತ್ರ ಕಲಿಕೆಗಾಗಿ ಬಳಸಲಾಗುವ, ಹೊರತೆಗೆಯಲಾದ ಮೆಟಾಡೇಟಾ ಹೊಂದಿರುವ ಮಧ್ಯಮ ಗಾತ್ರದ ಚಿತ್ರ", "image_preview_quality_description": "1-100 ವರೆಗಿನ ಪೂರ್ವವೀಕ್ಷಣೆ ಗುಣಮಟ್ಟ. ಹೆಚ್ಚಿನದು ಉತ್ತಮ, ಆದರೆ ದೊಡ್ಡ ಫೈಲ್‌ಗಳನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ ಮತ್ತು ಅಪ್ಲಿಕೇಶನ್ ಪ್ರತಿಕ್ರಿಯೆಯನ್ನು ಕಡಿಮೆ ಮಾಡುತ್ತದೆ. ಕಡಿಮೆ ಮೌಲ್ಯವನ್ನು ಹೊಂದಿಸುವುದು ಯಂತ್ರ ಕಲಿಕೆಯ ಗುಣಮಟ್ಟದ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರಬಹುದು.", "image_preview_title": "ಪೂರ್ವವೀಕ್ಷಣೆ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", + "image_progressive": "ಪ್ರಗತಿಪರ", + "image_progressive_description": "ಕ್ರಮೇಣ ಲೋಡಿಂಗ್ ಪ್ರದರ್ಶನಕ್ಕಾಗಿ JPEG ಚಿತ್ರಗಳನ್ನು ಹಂತಹಂತವಾಗಿ ಎನ್‌ಕೋಡ್ ಮಾಡಿ. ಇದು WebP ಚಿತ್ರಗಳ ಮೇಲೆ ಯಾವುದೇ ಪರಿಣಾಮ ಬೀರುವುದಿಲ್ಲ.", "image_quality": "ಗುಣಮಟ್ಟ", "image_resolution": "ರೆಸಲ್ಯೂಶನ್", "image_settings": "ಚಿತ್ರ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "image_settings_description": "ರಚಿಸಲಾದ ಚಿತ್ರಗಳ ಗುಣಮಟ್ಟ ಮತ್ತು ರೆಸಲ್ಯೂಶನ್ ಅನ್ನು ನಿರ್ವಹಿಸಿ", + "image_thumbnail_description": "ಮುಖ್ಯ ಟೈಮ್‌ಲೈನ್‌ನಂತಹ ಫೋಟೋಗಳ ಗುಂಪುಗಳನ್ನು ವೀಕ್ಷಿಸುವಾಗ ಬಳಸಲಾಗುವ ಸ್ಟ್ರಿಪ್ಡ್ ಮೆಟಾಡೇಟಾ ಹೊಂದಿರುವ ಸಣ್ಣ ಥಂಬ್‌ನೇಲ್", + "image_thumbnail_quality_description": "ಥಂಬ್‌ನೇಲ್ ಗುಣಮಟ್ಟ 1 ರಿಂದ 100 ರವರೆಗೆ. ಹೆಚ್ಚಿನದು ಉತ್ತಮ, ಆದರೆ ದೊಡ್ಡ ಫೈಲ್‌ಗಳನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ ಮತ್ತು ಅಪ್ಲಿಕೇಶನ್ ಪ್ರತಿಕ್ರಿಯೆಯನ್ನು ಕಡಿಮೆ ಮಾಡುತ್ತದೆ.", "image_thumbnail_title": "ಥಂಬ್‌ನೇಲ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "import_config_from_json_description": "JSON ಕಾನ್ಫಿಗರೇಶನ್ ಫೈಲ್ ಅನ್ನು ಅಪ್‌ಲೋಡ್ ಮಾಡುವ ಮೂಲಕ ಸಿಸ್ಟಮ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ಆಮದು ಮಾಡಿ", "job_concurrency": "{job} ಸಹವರ್ತಿತ್ವ", @@ -100,6 +111,7 @@ "job_settings": "ಕೆಲಸದ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "job_settings_description": "ಕೆಲಸದ ಸಮಕಾಲೀನತೆಯನ್ನು ನಿರ್ವಹಿಸಿ", "jobs_over_time": "ಕಾಲಾನಂತರದ ಉದ್ಯೋಗಗಳು", + "library_created": "ರಚಿಸಲಾದ ಲೈಬ್ರರಿ: {library}", "library_deleted": "ಲೈಬ್ರರಿಯನ್ನು ಅಳಿಸಲಾಗಿದೆ", "library_details": "ಲೈಬ್ರರಿಯ ವಿವರಗಳು", "library_folder_description": "ಆಮದು ಮಾಡಿಕೊಳ್ಳಲು ಒಂದು ಫೋಲ್ಡರ್ ಅನ್ನು ನಿರ್ದಿಷ್ಟಪಡಿಸಿ. ಉಪ ಫೋಲ್ಡರ್‌ಗಳನ್ನು ಒಳಗೊಂಡಂತೆ ಈ ಫೋಲ್ಡರ್ ಅನ್ನು ಚಿತ್ರಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳಿಗಾಗಿ ಸ್ಕ್ಯಾನ್ ಮಾಡಲಾಗುತ್ತದೆ.", @@ -122,10 +134,13 @@ "machine_learning_availability_checks_description": "ಲಭ್ಯವಿರುವ ಯಂತ್ರ ಕಲಿಕೆ ಸರ್ವರ್‌ಗಳನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಪತ್ತೆಹಚ್ಚಿ ಮತ್ತು ಆದ್ಯತೆ ನೀಡಿ", "machine_learning_availability_checks_enabled": "ಲಭ್ಯತೆ ಪರಿಶೀಲನೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", "machine_learning_availability_checks_interval": "ಮಧ್ಯಂತರವನ್ನು ಪರಿಶೀಲಿಸಿ", + "machine_learning_availability_checks_interval_description": "ಲಭ್ಯತೆ ಪರಿಶೀಲನೆಗಳ ನಡುವಿನ ಮಧ್ಯಂತರ ಮಿಲಿಸೆಕೆಂಡುಗಳಲ್ಲಿ", "machine_learning_availability_checks_timeout": "ವಿನಂತಿ ಅವಧಿ ಮೀರಿದೆ", + "machine_learning_availability_checks_timeout_description": "ಲಭ್ಯತೆ ಪರಿಶೀಲನೆಗಳಿಗಾಗಿ ಮಿಲಿಸೆಕೆಂಡುಗಳಲ್ಲಿ ಸಮಯ ಮೀರಿದೆ", "machine_learning_clip_model": "CLIP ಮಾದರಿ", "machine_learning_duplicate_detection": "ನಕಲು ಪತ್ತೆ", "machine_learning_duplicate_detection_enabled": "ನಕಲು ಪತ್ತೆಹಚ್ಚುವಿಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", + "machine_learning_duplicate_detection_enabled_description": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದರೆ, ನಿಖರವಾಗಿ ಒಂದೇ ರೀತಿಯ ಸ್ವತ್ತುಗಳನ್ನು ಇನ್ನೂ ಡಿ-ಡೂಪ್ಲಿಕೇಶನ್ ಮಾಡಲಾಗುತ್ತದೆ.", "machine_learning_duplicate_detection_setting_description": "ಸಂಭಾವ್ಯ ನಕಲುಗಳನ್ನು ಕಂಡುಹಿಡಿಯಲು CLIP ಎಂಬೆಡಿಂಗ್‌ಗಳನ್ನು ಬಳಸಿ", "machine_learning_enabled": "ಯಂತ್ರ ಕಲಿಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", "machine_learning_enabled_description": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಕೆಳಗಿನ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಲೆಕ್ಕಿಸದೆ ಎಲ್ಲಾ ML ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗುತ್ತದೆ.", @@ -133,24 +148,45 @@ "machine_learning_facial_recognition_description": "ಚಿತ್ರಗಳಲ್ಲಿ ಮುಖಗಳನ್ನು ಪತ್ತೆ ಮಾಡಿ, ಗುರುತಿಸಿ ಮತ್ತು ಗುಂಪು ಮಾಡಿ", "machine_learning_facial_recognition_model": "ಮುಖ ಗುರುತಿಸುವಿಕೆ ಮಾದರಿ", "machine_learning_facial_recognition_setting": "ಮುಖ ಗುರುತಿಸುವಿಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", + "machine_learning_facial_recognition_setting_description": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಚಿತ್ರಗಳನ್ನು ಮುಖ ಗುರುತಿಸುವಿಕೆಗಾಗಿ ಎನ್‌ಕೋಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ ಮತ್ತು ಎಕ್ಸ್‌ಪ್ಲೋರ್ ಪುಟದಲ್ಲಿ ಜನರ ವಿಭಾಗವನ್ನು ಭರ್ತಿ ಮಾಡುವುದಿಲ್ಲ.", + "machine_learning_max_detection_distance": "ಗರಿಷ್ಠ ಪತ್ತೆ ದೂರ", + "machine_learning_max_recognition_distance": "ಗರಿಷ್ಠ ಗುರುತಿಸುವಿಕೆ ದೂರ", + "machine_learning_min_detection_score": "ಕನಿಷ್ಠ ಪತ್ತೆ ಸ್ಕೋರ್", + "machine_learning_min_recognized_faces": "ಕನಿಷ್ಠ ಗುರುತಿಸಲ್ಪಟ್ಟ ಮುಖಗಳು", "machine_learning_ocr": "ಓಸಿಆರ್", + "machine_learning_ocr_description": "ಚಿತ್ರಗಳಲ್ಲಿನ ಪಠ್ಯವನ್ನು ಗುರುತಿಸಲು ಯಂತ್ರ ಕಲಿಕೆಯನ್ನು ಬಳಸಿ", "machine_learning_ocr_enabled": "OCR ಸಕ್ರಿಯಗೊಳಿಸಿ", "machine_learning_ocr_enabled_description": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಚಿತ್ರಗಳು ಪಠ್ಯ ಗುರುತಿಸುವಿಕೆಗೆ ಒಳಗಾಗುವುದಿಲ್ಲ.", "machine_learning_ocr_max_resolution": "ಗರಿಷ್ಠ ರೆಸಲ್ಯೂಷನ್", "machine_learning_ocr_max_resolution_description": "ಈ ರೆಸಲ್ಯೂಷನ್ ಮೇಲಿನ ಪೂರ್ವವೀಕ್ಷಣೆಗಳನ್ನು ಆಕಾರ ಅನುಪಾತವನ್ನು ಸಂರಕ್ಷಿಸುವಾಗ ಮರುಗಾತ್ರಗೊಳಿಸಲಾಗುತ್ತದೆ. ಹೆಚ್ಚಿನ ಮೌಲ್ಯಗಳು ಹೆಚ್ಚು ನಿಖರವಾಗಿರುತ್ತವೆ, ಆದರೆ ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲು ಮತ್ತು ಹೆಚ್ಚಿನ ಮೆಮೊರಿಯನ್ನು ಬಳಸಲು ಹೆಚ್ಚು ಸಮಯ ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ.", + "machine_learning_ocr_min_detection_score": "ಕನಿಷ್ಠ ಪತ್ತೆ ಸ್ಕೋರ್", "machine_learning_ocr_min_recognition_score": "ಕನಿಷ್ಠ ಡಿಟೆಕ್ಷನ್ ಅಂಕ", "machine_learning_ocr_model": "OCR ಮಾಡೆಲ್", + "machine_learning_ocr_model_description": "ಸರ್ವರ್ ಮೋಡೆಲ್ಗಳು ಮೊಬೈಲ್ ಮೋಡೆಲ್ಗಳಿಗಿಂತ ಹೆಚ್ಚು ನಿಖರವಾಗಿರುತ್ತವೆ, ಆದರೆ ಹೆಚ್ಚಿನ ಮೆಮೊರಿಯನ್ನು ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲು ಮತ್ತು ಬಳಸಲು ಹೆಚ್ಚು ಸಮಯ ತೆಗೆದುಕೊಳ್ಳುತ್ತವೆ.", "machine_learning_settings": "ಯಂತ್ರ ಕಲಿಕೆ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "machine_learning_settings_description": "ಯಂತ್ರ ಕಲಿಕೆ ವೈಶಿಷ್ಟ್ಯಗಳು ಮತ್ತು ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ", "machine_learning_smart_search": "ಸ್ಮಾರ್ಟ್ ಹುಡುಕಾಟ", "machine_learning_smart_search_description": "CLIP ಎಂಬೆಡಿಂಗ್‌ಗಳನ್ನು ಬಳಸಿಕೊಂಡು ಚಿತ್ರಗಳನ್ನು ಅರ್ಥಪೂರ್ಣವಾಗಿ ಹುಡುಕಿ", "machine_learning_smart_search_enabled": "ಸ್ಮಾರ್ಟ್ ಹುಡುಕಾಟವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", "machine_learning_smart_search_enabled_description": "ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಚಿತ್ರಗಳನ್ನು ಸ್ಮಾರ್ಟ್ ಹುಡುಕಾಟಕ್ಕಾಗಿ ಎನ್‌ಕೋಡ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ.", + "maintenance_delete_backup": "ಬ್ಯಾಕಪ್ ಅಳಿಸಿ", + "maintenance_delete_backup_description": "ಈ ಫೈಲ್ ಅನ್ನು ಶಾಶ್ವತವಾಗಿ ಅಳಿಸಲಾಗುತ್ತದೆ.", + "maintenance_delete_error": "ಬ್ಯಾಕಪ್ ಅಳಿಸಲು ವಿಫಲವಾಗಿದೆ.", + "maintenance_restore_backup": "ಬ್ಯಾಕಪ್ ಅನ್ನು ಮರುಸ್ಥಾಪಿಸಿ", + "maintenance_restore_backup_description": "ಆಯ್ಕೆ ಮಾಡಿದ ಬ್ಯಾಕಪ್‌ನಿಂದ ಇಮ್ಮಿಚ್ ಅನ್ನು ಅಳಿಸಿಹಾಕಲಾಗುತ್ತದೆ ಮತ್ತು ಮರುಸ್ಥಾಪಿಸಲಾಗುತ್ತದೆ. ಮುಂದುವರಿಯುವ ಮೊದಲು ಬ್ಯಾಕಪ್ ಅನ್ನು ರಚಿಸಲಾಗುತ್ತದೆ.", + "maintenance_restore_backup_different_version": "ಈ ಬ್ಯಾಕಪ್ ಅನ್ನು ಇಮ್ಮಿಚ್‌ನ ವಿಭಿನ್ನ ಆವೃತ್ತಿಯೊಂದಿಗೆ ರಚಿಸಲಾಗಿದೆ!", + "maintenance_restore_backup_unknown_version": "ಬ್ಯಾಕಪ್ ಆವೃತ್ತಿಯನ್ನು ನಿರ್ಧರಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ.", + "maintenance_restore_database_backup": "ಡೇಟಾಬೇಸ್ ಬ್ಯಾಕಪ್ ಮರುಸ್ಥಾಪಿಸಿ", + "maintenance_restore_database_backup_description": "ಬ್ಯಾಕಪ್ ಫೈಲ್ ಬಳಸಿ ಹಿಂದಿನ ಡೇಟಾಬೇಸ್ ಸ್ಥಿತಿಗೆ ಹಿಂತಿರುಗಿ", "maintenance_settings": "ನಿರ್ವಹಣೆ", "maintenance_settings_description": "ಇಮ್ಮಿಚ್ ಅನ್ನು ನಿರ್ವಹಣಾ ಕ್ರಮಕ್ಕೆ ಇರಿಸಿ.", "maintenance_start": "ನಿರ್ವಹಣಾ ಮೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ", "maintenance_start_error": "ನಿರ್ವಹಣಾ ಕ್ರಮವನ್ನು ಪ್ರಾರಂಭಿಸಲು ವಿಫಲವಾಗಿದೆ.", + "maintenance_upload_backup": "ಡೇಟಾಬೇಸ್ ಬ್ಯಾಕಪ್ ಫೈಲ್ ಅಪ್‌ಲೋಡ್ ಮಾಡಿ", + "manage_concurrency": "ಏಕಕಾಲಿಕತೆಯನ್ನು ನಿರ್ವಹಿಸಿ", + "manage_concurrency_description": "ಉದ್ಯೋಗ ಸಮಕಾಲೀನತೆಯನ್ನು ನಿರ್ವಹಿಸಲು ಉದ್ಯೋಗಗಳ ಪುಟಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ", "manage_log_settings": "ಲಾಗ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ", + "map_dark_style": "ಡಾರ್ಕ್ ಶೈಲಿ", "map_enable_description": "ನಕ್ಷೆ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", "map_gps_settings": "ನಕ್ಷೆ ಮತ್ತು GPS ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "map_reverse_geocoding": "ರಿವರ್ಸ್ ಜಿಯೋಕೋಡಿಂಗ್", @@ -158,10 +194,12 @@ "map_reverse_geocoding_settings": "ರಿವರ್ಸ್ ಜಿಯೋಕೋಡಿಂಗ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "map_settings": "ನಕ್ಷೆ", "map_settings_description": "ನಕ್ಷೆ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ", + "memory_cleanup_job": "ಮೆಮೊರಿ ಸ್ವಚ್ಛಗೊಳಿಸುವಿಕೆ", "memory_generate_job": "ಸ್ಮೃತಿ ಉತ್ಪಾದನೆ", "metadata_extraction_job": "ಮೆಟಾಡೇಟಾವನ್ನು ಹೊರತೆಗೆಯಿರಿ", "metadata_extraction_job_description": "GPS, ಮುಖಗಳು ಮತ್ತು ರೆಸಲ್ಯೂಶನ್‌ನಂತಹ ಪ್ರತಿ ಸ್ವತ್ತಿನಿಂದ ಮೆಟಾಡೇಟಾ ಮಾಹಿತಿಯನ್ನು ಹೊರತೆಗೆಯಿರಿ", "metadata_faces_import_setting": "ಮುಖ ಆಮದು ಸಕ್ರಿಯಗೊಳಿಸಿ", + "metadata_faces_import_setting_description": "ಇಮೇಜ್ EXIF ಡೇಟಾ ಮತ್ತು ಸೈಡ್‌ಕಾರ್ ಫೈಲ್‌ಗಳಿಂದ ಮುಖಗಳನ್ನು ಆಮದು ಮಾಡಿ", "metadata_settings": "ಮೆಟಾಡೇಟಾ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", "metadata_settings_description": "ಮೆಟಾಡೇಟಾ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ", "migration_job": "ವಲಸೆ", @@ -179,6 +217,7 @@ "nightly_tasks_start_time_setting_description": "ಸರ್ವರ್ ರಾತ್ರಿಯ ಕಾರ್ಯಗಳನ್ನು ನಡೆಸಲು ಪ್ರಾರಂಭಿಸುವ ಸಮಯ", "nightly_tasks_sync_quota_usage_setting": "ಸಿಂಕ್ ಕೋಟಾ ಬಳಕೆ", "nightly_tasks_sync_quota_usage_setting_description": "ಪ್ರಸ್ತುತ ಬಳಕೆಯ ಆಧಾರದ ಮೇಲೆ ಬಳಕೆದಾರರ ಸಂಗ್ರಹಣಾ ಕೋಟಾವನ್ನು ನವೀಕರಿಸಿ", + "no_paths_added": "ಯಾವುದೇ ಮಾರ್ಗಗಳನ್ನು ಸೇರಿಸಲಾಗಿಲ್ಲ", "no_pattern_added": "ಯಾವುದೇ ಪ್ಯಾಟರ್ನ್ ಸೇರಿಸಲಾಗಿಲ್ಲ", "note_cannot_be_changed_later": "ಗಮನಿಸಿ: ಇದನ್ನು ನಂತರ ಬದಲಾಯಿಸಲಾಗುವುದಿಲ್ಲ!", "notification_email_from_address": "ವಿಳಾಸದಿಂದ", @@ -206,8 +245,17 @@ "template_email_preview": "ಪೂರ್ವವೀಕ್ಷಣೆ", "transcoding_tone_mapping": "ಟೋನ್-ಮ್ಯಾಪಿಂಗ್" }, + "admin_email": "ನಿರ್ವಾಹಕ ಇಮೇಲ್", + "admin_password": "ನಿರ್ವಾಹಕ ಪಾಸ್‌ವರ್ಡ್", "administration": "ಆಡಳಿತ", "advanced": "ಸುಧಾರಿತ", + "album": "ಆಲ್ಬಮ್", + "album_added": "ಆಲ್ಬಮ್ ಸೇರಿಸಲಾಗಿದೆ", + "album_cover_updated": "ಆಲ್ಬಮ್ ಕವರ್ ನವೀಕರಿಸಲಾಗಿದೆ", + "album_deleted": "ಆಲ್ಬಮ್ ಅಳಿಸಲಾಗಿದೆ", + "album_info_updated": "ಆಲ್ಬಮ್ ಮಾಹಿತಿಯನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ", + "album_name": "ಆಲ್ಬಮ್ ಹೆಸರು", + "album_summary": "ಆಲ್ಬಮ್ ಸಾರಾಂಶ", "albums": "ಆಲ್ಬಂಗಳು", "all": "ಎಲ್ಲವೂ", "anti_clockwise": "ಅಪ್ರದಕ್ಷಿಣಾಕಾರವಾಗಿ", diff --git a/i18n/ko.json b/i18n/ko.json index 147ace091d..5449bf1e44 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -1581,7 +1581,6 @@ "not_available": "없음", "not_in_any_album": "앨범에 없음", "not_selected": "선택되지 않음", - "note_apply_storage_label_to_previously_uploaded assets": "참고: 이전에 업로드한 항목에도 스토리지 레이블을 적용하려면 다음을 실행합니다,", "notes": "참고", "nothing_here_yet": "아직 아무것도 없음", "notification_permission_dialog_content": "알림을 활성화하려면 설정에서 알림 권한을 허용하세요.", @@ -1780,7 +1779,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {항목 #개} other {항목 #개}}를 새 인물에게 재지정했습니다.", "reassing_hint": "기존 인물에 선택한 항목 할당", "recent": "최근", - "recent-albums": "최근 앨범", + "recent_albums": "최근 앨범", "recent_searches": "최근 검색", "recently_added": "최근 추가", "recently_added_page_title": "최근 추가", diff --git a/i18n/lt.json b/i18n/lt.json index 2802bb58ab..fec5905957 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -18,6 +18,7 @@ "add_a_title": "Pridėti pavadinimą", "add_action": "Pridėti veiksmą", "add_action_description": "Spustelėkite, kad pridėtumėte veiksmą atlikimui", + "add_assets": "Pridėti", "add_birthday": "Pridėti gimimo diena", "add_endpoint": "Pridėti galutinį tašką", "add_exclusion_pattern": "Pridėti išimčių šabloną", @@ -32,9 +33,9 @@ "add_to": "Pridėti į…", "add_to_album": "Pridėti į albumą", "add_to_album_bottom_sheet_added": "Pridėta į {album}", - "add_to_album_bottom_sheet_already_exists": "Jau yra albume {album}", + "add_to_album_bottom_sheet_already_exists": "Jau yra {album}", "add_to_album_bottom_sheet_some_local_assets": "Dalis vietinių elementų negalėjo būti pridėti į albumą", - "add_to_album_toggle": "Perjungti pažymėjimus albumui {album}", + "add_to_album_toggle": "Perjungti pasirinkimus šiam {album}", "add_to_albums": "Pridėti į albumus", "add_to_albums_count": "Pridėti į albumus ({count})", "add_to_bottom_bar": "Pridėti prie", @@ -103,6 +104,8 @@ "image_preview_description": "Vidutinio dydžio vaizdas su išvalytais metaduomenimis, naudojamas kai žiūrimas vienas objektas arba mašininiam mokymuisi", "image_preview_quality_description": "Peržiūros kokybė nuo 1-100. Aukštesnės reikšmės yra geriau, bet sukuriami didesni failai gali sumažinti programos reagavimo laiką. Mažos vertės nustatymas gali paveikti mašininio mokymo kokybę.", "image_preview_title": "Peržiūros nustatymai", + "image_progressive": "Progresyvus", + "image_progressive_description": "JPEG vaizdus koduokite progresyviai, kad vaizdai krautųsi laipsniškai. Tai neturi jokios įtakos WebP nuotraukoms.", "image_quality": "Kokybė", "image_resolution": "Rezoliucija", "image_resolution_description": "Didesnės rezoliucijos gali išsaugoti daugiau detalių, bet ilgiau užtrunka užkoduoti, failai yra didesni ir programos reagavimo laikas gali sumažėti.", @@ -134,7 +137,7 @@ "library_tasks_description": "Skenuoti išorines bibliotekas, ieškant naujų arba pakeistų išteklių", "library_updated": "Atnaujinta biblioteka", "library_watching_enable_description": "Stebėti išorines bibliotekas dėl failų pakeitimų", - "library_watching_settings": "Bibliotekų stebėjimas (EKSPERIMENTINIS", + "library_watching_settings": "Bibliotekų stebėjimas [EKSPERIMENTINIS]", "library_watching_settings_description": "Automatiškai stebėti dėl pakeistų failų", "logging_enable_description": "Įjungti žurnalo vedimą", "logging_level_description": "Įjungus, kokį žurnalo vedimo lygį naudot.", @@ -187,10 +190,21 @@ "machine_learning_smart_search_enabled": "Įjungti išmaniąją paiešką", "machine_learning_smart_search_enabled_description": "Jei išjungta, vaizdai nebus užkoduoti išmaniajai paieškai.", "machine_learning_url_description": "Mašininio mokymosi serverio URL. Jei pateikta daugiau nei vienas URL, serveriai bus bandomi eilės tvarka nuo pirmo iki paskutinio tol, kol bus rastas vienas veikiantis serveris.", + "maintenance_delete_backup": "Ištrinti atsarginę kopiją", + "maintenance_delete_backup_description": "Šis failas bus negrįžtamai ištrintas.", + "maintenance_delete_error": "Nepavyko ištrinti atsarginės kopijos.", + "maintenance_restore_backup": "Atstatyti atsarginę kopiją", + "maintenance_restore_backup_description": "Immich bus ištrintas ir atkurtas iš pasirinktos atsarginės kopijos. Prieš tęsiant bus sukurta atsarginė kopija.", + "maintenance_restore_backup_different_version": "Ši atsarginė kopija buvo sukurta su skirtinga Immich versija!", + "maintenance_restore_backup_unknown_version": "Nepavyko nustatyti atsarginės kopijos versijos.", + "maintenance_restore_database_backup": "Atstatyti duomenų bazę", + "maintenance_restore_database_backup_description": "Grąžinti į ankstesnę duomenų bazės būseną naudojant atsarginę kopiją", "maintenance_settings": "Aptarnavimas", "maintenance_settings_description": "Perjungti „Immich“ į aptarnavimo režimą.", - "maintenance_start": "Paleisti aptarnavimo režimą", + "maintenance_start": "Perjungti į aptarnavimo režimą", "maintenance_start_error": "Nepavyko paleisti aptarnavimo režimo.", + "maintenance_upload_backup": "Išsiųsti atsarginę duomenų bazės kopiją", + "maintenance_upload_backup_error": "Nepavyko išsiųsti atsarginės kopijos, ar tai .sql/.sql.gz failas?", "manage_concurrency": "Tvarkyti lygiagretumą", "manage_concurrency_description": "Eikite į darbų puslapį, kad galėtumėte valdyti darbų lygiagretumą", "manage_log_settings": "Valdyti žurnalo nuostatas", @@ -258,7 +272,7 @@ "oauth_auto_register": "Automatinis registravimas", "oauth_auto_register_description": "Automatiškai užregistruoti naujus naudotojus po prisijungimo per OAuth", "oauth_button_text": "Mygtuko tekstas", - "oauth_client_secret_description": "Privalomas jei PKCE (Proof Key for Code Exchange) nepalaikomas pagal OAuth tiekėją", + "oauth_client_secret_description": "Privalomas konfidencialaus kliento, arba jei viešasis klientas nepalaiko PKCE (Proof Key for Code Exchange).", "oauth_enable_description": "Prisijungti su OAuth", "oauth_mobile_redirect_uri": "Mobiliojo peradresavimo URI", "oauth_mobile_redirect_uri_override": "Mobiliojo peradresavimo URI pakeitimas", @@ -437,6 +451,9 @@ "admin_password": "Administratoriaus slaptažodis", "administration": "Administravimas", "advanced": "Sudėtingesnis", + "advanced_settings_clear_image_cache": "Išvalyti vaizdo talpyklą", + "advanced_settings_clear_image_cache_error": "Nepavyko išvalyti vaizdo taupyklos", + "advanced_settings_clear_image_cache_success": "Sėkmingai išvalyta {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Naudokite šį nustatymą medijos filtravimui sinchronizuojant remiantis alternatyviais kriterijais. Naudokite tik jei programa turi problemų su visų albumų aptikimu.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTINIS] Naudokite alternatyvų įrenginio albumų sinchronizavimo filtrą", "advanced_settings_log_level_title": "Žurnalo įrašų lygis: {level}", @@ -478,6 +495,7 @@ "album_summary": "Albumo santrauka", "album_updated": "Albumas atnaujintas", "album_updated_setting_description": "Gauti pranešimą el. paštu, kai bendrinamas albumas turi naujų elementų", + "album_upload_assets": "Įkelkite elementus iš savo kompiuterio ir pridėkite prie albumo", "album_user_left": "Paliko {album}", "album_user_removed": "Pašalintas {user}", "album_viewer_appbar_delete_confirm": "Ar tikrai norite ištrinti šį albumą iš savo paskyros?", @@ -499,6 +517,7 @@ "all": "Visi", "all_albums": "Visi albumai", "all_people": "Visi žmonės", + "all_photos": "Visos nuotraukos", "all_videos": "Visi video", "allow_dark_mode": "Leisti tamsųjį režimą", "allow_edits": "Leisti redagavimus", @@ -506,7 +525,10 @@ "allow_public_user_to_upload": "Leisti viešam naudotojui įkelti", "allowed": "Leidžiama", "alt_text_qr_code": "QR kodo paveiksliukas", - "anti_clockwise": "Prieš laikrodžio rodykles", + "always_keep": "Visada laikyti", + "always_keep_photos_hint": "Atlaisvinti Vietos išsaugos visas foto šiame įrenginyje.", + "always_keep_videos_hint": "Atlaisvinti Vietos išsaugos visas video šiame įrenginyje.", + "anti_clockwise": "Prieš laikrodžio rodyklę", "api_key": "API raktas", "api_key_description": "Ši reikšmė bus parodyta tik vieną kartą. Prašome nusikopijuoti prieš uždarant šį langą.", "api_key_empty": "Jūsų API rakto pavadinimas netūrėtų būti tuščias", @@ -550,6 +572,9 @@ "asset_list_layout_sub_title": "Išdėstymas", "asset_list_settings_subtitle": "Nuotraukų tinklelio išdėstymo nustatymai", "asset_list_settings_title": "Nuotraukų tinklelis", + "asset_not_found_on_device_android": "Įrenginyje elementas nerastas", + "asset_not_found_on_device_ios": "Įrenginyje elementas nerastas. Jei naudojate iCloud, elementas gali būti nepasiekiamas dėl blogo failo, saugomo iCloud", + "asset_not_found_on_icloud": "Elementas nerastas iCloud. Elementas gali būti nepasiekiamas dėl blogo failo, saugomo iCloud", "asset_offline": "Elementas nepasiekiamas", "asset_offline_description": "Šis išorinis elementas neberandamas diske. Dėl pagalbos susisiekite su savo Immich administratoriumi.", "asset_restored_successfully": "Elementas atkurtas sėkmingai", @@ -568,20 +593,20 @@ "assets_cannot_be_added_to_album_count": "{count, plural, one {Elementas negali būti pridėtas} few {Elementai negali būti pridėti} other {Elementų negali būti pridėta}} į albumą", "assets_cannot_be_added_to_albums": "{count, plural, one {Elementas negali būti pridėtas} few {Elementai negali būti pridėti} other {Elementų negali būti pridėta}} į nei vieną albumą", "assets_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}}", - "assets_deleted_permanently": "{count} elementų ištrinta galutinai", - "assets_deleted_permanently_from_server": "{count} elementų ištrinta galutinai iš Immich serverio", + "assets_deleted_permanently": "{count} {count, plural, one {elementas ištrintas} few {elementai ištrinti} other {elementų ištrinta}} galutinai", + "assets_deleted_permanently_from_server": "{count} {count, plural, one {elementas ištrintas} few {elementai ištrinti} other {elementų ištrinta}} galutinai iš Immich serverio", "assets_downloaded_failed": "{count, plural, one {Atsisiųstas # failas - {error} failas nepavyko} few {Atsisiųsti # failai - {error} failai nepavyko} other {Atsisiųsta # failų - {error} failų nepavyko}}", "assets_downloaded_successfully": "{count, plural, one {Atsisiųstas # failas sėkmingai} few {Atsisiųsti # failai sėkmingai} other {Atsisiųsta # failų sėkmingai}}", "assets_moved_to_trash_count": "{count, plural, one {# elementas perkeltas} few {# elementai perkelti} other {# elementų perkelta}} į šiukšliadėžę", "assets_permanently_deleted_count": "{count, plural, one {# elementas ištrintas} few {# elementai ištrinti} other {# elementų ištrinta}} visam laikui", "assets_removed_count": "{count, plural, one {Pašalintas # elementas} few {Pašalinti # elementai} other {Pašalinta # elementų}}", - "assets_removed_permanently_from_device": "{count} elementų pašalinta galutinai iš jūsų įrenginio", + "assets_removed_permanently_from_device": "{count} {count, plural, one {elementas pašalintas} few {elementai pašalinti} other {elementų pašalinta}} galutinai iš jūsų įrenginio", "assets_restore_confirmation": "Ar tikrai norite atkurti visus šiukšliadėžėje esančius perkeltus elementus? Šio veiksmo atšaukti negalėsite! Pastaba: nepasiekiami elementai tokiu būdu atkurti nebus.", "assets_restored_count": "{count, plural, one {Atkurtas # elementas} few {Atkurti # elementai} other {Atkurta # elementų}}", - "assets_restored_successfully": "{count} element(as, ai, ų) atkurta sėkmingai", - "assets_trashed": "{count} element(ai,ų,as) perkelta į šiukšliadėžę", + "assets_restored_successfully": "{count} {count, plural, one {elementas atkurtas} few {elementai atkurti} other {elementų atkurta}} sėkmingai", + "assets_trashed": "{count} {count, plural, one {elementas perkeltas} few {elementai perkelti} other {elementų perkelta}} į šiukšliadėžę", "assets_trashed_count": "Perkelta į šiukšliadėžę {count, plural, one {# elementas} few {# elementai} other {# elementų}}", - "assets_trashed_from_server": "{count} element(as, ai, ų) perkelta į šiukšliadėžę iš Immich serverio", + "assets_trashed_from_server": "{count} {count, plural, one {elementas perkeltas} few {elementai perkelti} other {elementų perkelta}} į šiukšliadėžę iš Immich serverio", "assets_were_part_of_album_count": "{count, plural, one {# elementas} few {# elementai} other {# elementų}} jau prieš tai buvo albume", "assets_were_part_of_albums_count": "{count, plural, one {Elementas } few {Elementai} other {Elementų}} jau buvo albumuose", "authorized_devices": "Autorizuoti įrenginiai", @@ -673,6 +698,7 @@ "blurred_background": "Neryškus fonas", "bugs_and_feature_requests": "Klaidų ir funkcijų užklausos", "build": "Versija", + "build_image": "Programos Paketas", "bulk_delete_duplicates_confirmation": "Ar tikrai norite ištrinti visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančių elementų}}? Bus paliktas didžiausias kiekvienos grupės elementas ir negrįžtamai ištrinti kiti besidubliuojantys elementai. Šio veiksmo atšaukti negalėsite!", "bulk_keep_duplicates_confirmation": "Ar tikrai norite palikti visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančių elementų}}? Tokiu būdu nieko netrinant bus sutvarkytos visos dublikatų grupės.", "bulk_trash_duplicates_confirmation": "Ar tikrai norite perkelti į šiukšliadėžę visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančių elementų}}? Bus paliktas didžiausias kiekvienos grupės elementas ir į šiukšliadėžę perkelti kiti besidubliuojantys elementai.", @@ -733,6 +759,15 @@ "checksum": "„Checksum“", "choose_matching_people_to_merge": "Pasirinkite atitinkančius žmones sujungimui", "city": "Miestas", + "cleanup_confirm_description": "Immich rado {count} {count, plural, one {elementą (sukurtą iki {date}), kurio atsarginė kopija jau išsaugota serveryje. Pašalinti vietinę kopiją iš šio įrenginio} few {elementai (sukurti iki {date}), kurių atsarginės kopijos jau išsaugotos serveryje. Pašalinti vietines kopijas iš šio įrenginio} other {elementų (sukurtų iki {date}), kurių atsarginės kopijos jau išsaugotos serveryje. Pašalinti vietines kopijas iš šio įrenginio}}?", + "cleanup_confirm_prompt_title": "Ištrinti iš šio įrenginio?", + "cleanup_deleted_assets": "Išmesti {count} {count, plural, one {elementą} few {elementus} other {elementų}} į šiukšlinę", + "cleanup_deleting": "Metama į šiukšlinę...", + "cleanup_found_assets": "Rasta {count} {count, plural, one {išsaugotas elementas} few {išsaugoti elementai} other {išsaugotų elementų}}", + "cleanup_found_assets_with_size": "Rasta {count} {count, plural, one {išsaugotas elementas} few {išsaugoti elementai} other {išsaugotų elementų}} ({size})", + "cleanup_no_assets_found": "Nerasta elementų, atitinkančių aukščiau pateiktus kriterijus. Atlaisvinti Vietos gali pašalinti tik tuos išteklius, kurių atsarginės kopijos yra serveryje", + "cleanup_preview_title": "Elementų ištrinti ({count})", + "cleanup_step4_summary": "{count} {count, plural, one {elementas (sukurtas iki {date}), kurį reikia pašalinti iš vietinio įrenginio. Nuotrauka liks pasiekiama Immich galerijoje} few {elementai (sukurti iki {date}), kuriuos reikia pašalinti iš vietinio įrenginio. Nuotraukos liks pasiekiamos Immich galerijoje} other {elementų (sukurtų iki {date}), kuriuos reikia pašalinti iš vietinio įrenginio. Nuotraukos liks pasiekiamos Immich galerijoje}}.", "clear": "Išvalyti", "clear_all": "Išvalyti viską", "clear_all_recent_searches": "Išvalyti visas naujausias paieškas", @@ -868,7 +903,7 @@ "delete_key": "Ištrinti raktą", "delete_library": "Ištrinti biblioteką", "delete_link": "Ištrinti nuorodą", - "delete_local_action_prompt": "{count} ištrinti vietiniame įrenginyje", + "delete_local_action_prompt": "{count} ištrinta vietiniame įrenginyje", "delete_local_dialog_ok_backed_up_only": "Ištrinti tik turinčius atsarginę kopiją", "delete_local_dialog_ok_force": "Vis tiek ištrinti", "delete_others": "Ištrinti kitus", @@ -903,7 +938,7 @@ "documentation": "Dokumentacija", "done": "Atlikta", "download": "Atsisiųsti", - "download_action_prompt": "Atsisiunčiami {count} elementai", + "download_action_prompt": "Atsisiunčiama {count} {count, plural, one {elementas} few {elementai} other {elementų}}", "download_canceled": "Atsisiuntimas atšauktas", "download_complete": "Atsisiuntimas pabaigtas", "download_enqueue": "Atsisiuntimai įtraukti į eilę", @@ -935,7 +970,7 @@ "edit_birthday": "Redaguoti gimtadienį", "edit_date": "Redaguoti datą", "edit_date_and_time": "Redaguoti datą ir laiką", - "edit_date_and_time_action_prompt": "{count} data ir laikas redaguotas", + "edit_date_and_time_action_prompt": "{count} {count, plural, one {data ir laikas redaguotas} few {datos ir laikai redaguoti} other {datų ir laikų redaguota}}", "edit_date_and_time_by_offset": "Keisti datą pagal poslinkį", "edit_date_and_time_by_offset_interval": "Naujas datos intervalas: {from} - {to}", "edit_description": "Redaguoti aprašymą", @@ -945,7 +980,7 @@ "edit_key": "Redaguoti raktą", "edit_link": "Redaguoti nuorodą", "edit_location": "Redaguoti vietovę", - "edit_location_action_prompt": "{count} vietovės pakeistos", + "edit_location_action_prompt": "{count} {count, plural, one {vietovė pakeista} few {vietovės pakeistos} other {vietovių pakeista}}", "edit_location_dialog_title": "Vietovė", "edit_name": "Redaguoti vardą", "edit_people": "Redaguoti žmones", @@ -1146,7 +1181,7 @@ "failed_to_load_assets": "Nepavyko įkelti elementų", "failed_to_load_folder": "Nepavyko įkelti katalogą", "favorite": "Mėgstamiausias", - "favorite_action_prompt": "{count} pridėta prie mėgstamiausių", + "favorite_action_prompt": "{count} {count, plural, one {pridėtas} few {pridėti} other {pridėta}} prie mėgstamiausių", "favorite_or_unfavorite_photo": "Įtraukti prie arba pašalinti iš mėgstamiausių", "favorites": "Mėgstamiausi", "favorites_page_no_favorites": "Nerasta mėgstamiausių elementų", @@ -1171,9 +1206,9 @@ "folders_feature_description": "Peržiūrėkite failų sistemoje esančias nuotraukas ir vaizdo įrašus aplankų rodinyje", "forgot_pin_code_question": "Pamiršote savo PIN?", "forward": "Pirmyn", - "free_up_space": "Atlaisvinti vietos", - "free_up_space_description": "Perkelkite atsargines nuotraukų ir vaizdo įrašų kopijas į įrenginio šiukšliadėžę, kad atlaisvintumėte vietos. Jūsų kopijos serveryje lieka saugios", - "free_up_space_settings_subtitle": "Atlaisvinkite įrenginio saugyklą", + "free_up_space": "Atlaisvinti Vietos", + "free_up_space_description": "Perkelkite atsargines nuotraukų ir vaizdo įrašų kopijas į įrenginio šiukšliadėžę, kad atlaisvintumėte vietos. Jūsų kopijos serveryje lieka saugios.", + "free_up_space_settings_subtitle": "Atlaisvinkite vietos įrenginyje", "full_path": "Pilnas kelias: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Kad veiktų, ši funkcija įkelia išorinius „Google“ išteklius.", @@ -1289,9 +1324,15 @@ "json_editor": "JSON redagavimas", "json_error": "JSON klaida", "keep": "Palikti", + "keep_albums": "Palikti albumus", + "keep_albums_count": "{count, plural, one {Paliekamas {count} albumas} few {Paliekami {count} albumai} other {Paliekama {count} albumų}}", "keep_all": "Palikti visus", + "keep_description": "Pasirinkite, kas lieka jūsų įrenginyje atlaisvinus vietos.", "keep_favorites": "Palikti mėgstamiausius", + "keep_on_device": "Palikti įrenginyje", + "keep_on_device_hint": "Pasirinkite elementus, kuriuos norite palikti šiame įrenginyje", "keep_this_delete_others": "Išsaugoti šį, kitus ištrinti", + "keeping": "Paliekama: {items}", "kept_this_deleted_others": "Išsaugotas šis elementas ir {count, plural, one {ištrintas # elementas} few {ištrinti # elementai} other {ištrinta # elementų}}", "keyboard_shortcuts": "Spartieji klaviatūros klavišai", "language": "Kalba", @@ -1389,6 +1430,7 @@ "maintenance_end": "Baigti techninę priežiūrą", "maintenance_end_error": "Nepavyko išjungti techninės priežiūros režimo.", "maintenance_logged_in_as": "Šiuo metu prisijungę kaip {user}", + "maintenance_restore_library_folder_has_files": "{folder} turi {count} {count, plural, one {aplanką} few {aplankus} other {aplankų}}", "maintenance_title": "Laikinai Neprieinamas", "make": "Gamintojas", "manage_geolocation": "Tvarkyti vietovę", @@ -1532,16 +1574,16 @@ "no_results_description": "Pabandykite sinonimą arba bendresnį raktažodį", "no_shared_albums_message": "Sukurkite nuotraukų ar vaizdo įrašų albumą dalinimuisi su žmonėmis jūsų tinkle", "no_uploads_in_progress": "Nėra vykstančių įkėlimų", + "none": "Niekas", "not_allowed": "Neleidžiama", "not_available": "Nepasiekiamas", "not_in_any_album": "Nė viename albume", "not_selected": "Nepasirinkta", - "note_apply_storage_label_to_previously_uploaded assets": "Pastaba: Priskirti Saugyklos Žymą prie anksčiau įkeltų ištekliu, paleiskite šį", "notes": "Pastabos", "nothing_here_yet": "Kol kas tuščia", "notification_permission_dialog_content": "Pranešimų įgalinimui eikite į Nustatymus ir pasirinkite Leisti.", "notification_permission_list_tile_content": "Suteikti leidimą pranešimų įgalinimui.", - "notification_permission_list_tile_enable_button": "Įgalinti pranešimus", + "notification_permission_list_tile_enable_button": "Įjungti pranešimus", "notification_permission_list_tile_title": "Pranešimų leidimai", "notification_toggle_setting_description": "Įjungti el. pašto pranešimus", "notifications": "Pranešimai", @@ -1553,7 +1595,7 @@ "official_immich_resources": "Oficialūs Immich ištekliai", "offline": "Neprisijungęs", "offset": "Ofsetas", - "ok": "Ok", + "ok": "Gerai", "oldest_first": "Seniausias pirmas", "on_this_device": "Šiame įrenginyje", "onboarding": "Įdarbinimas", @@ -1581,6 +1623,7 @@ "other_variables": "Kiti kintamieji", "owned": "Nuosavi", "owner": "Savininkas", + "page": "Puslapis", "partner": "Partneris", "partner_can_access": "{partner} gali naudotis", "partner_can_access_assets": "Visos jūsų nuotraukos ir vaizdo įrašai, išskyrus archyvuotus ir ištrintus", @@ -1613,12 +1656,13 @@ "people": "Asmenys", "people_edits_count": "{count, plural, one {Redaguotas # asmuo} few {Redaguoti # asmenys} other {Redaguota # asmenų}}", "people_feature_description": "Peržiūrėkite nuotraukas ir vaizdo įrašus sugrupuotus pagal asmenis", + "people_selected": "{count, plural, one {# asmuo pasirinktas} other {# asmenų pasirinkta}}", "people_sidebar_description": "Rodyti asmenų rodinio nuorodą šoninėje juostoje", "permanent_deletion_warning": "Ištrynimo visam laikui perspėjimas", "permanent_deletion_warning_setting_description": "Rodyti perspėjimą kai elementas ištrinamas visam laikui", "permanently_delete": "Ištrinti visam laikui", "permanently_delete_assets_count": "Visam laikui ištrinti {count, plural, one {# elementą} few {# elementus} other {# elementų}}", - "permanently_delete_assets_prompt": "Ar tikrai norite visam laikui ištrinti {count, plural, one {šitą elementą?} other {šituos # elementus?} other {šitų # elementų?}} Tuo pačiu {count, plural, one {jis bus pašalintas} other {jie bus pašalinti}} iš albumo(ų).", + "permanently_delete_assets_prompt": "Ar tikrai norite visam laikui ištrinti {count, plural, one {šį elementą?} other {šiuos # elementus?}} Tai bus tuo pačiu pašalinta {count, plural, one {iš} other {iš jų}} albumo(ų).", "permanently_deleted_asset": "Visiškai ištrinti elementai", "permanently_deleted_assets_count": "Visam laikui {count, plural, one {ištrintas # elementas} few {ištrinti # elementai} other {ištrinta # elementų}}", "permission": "Leidimas", @@ -1637,12 +1681,17 @@ "person_age_years": "{years, plural, other {# metų}} amžiaus", "person_birthdate": "Gimė {date}", "person_hidden": "{name}{hidden, select, true { (paslėptas)} other {}}", + "person_recognized": "Asmuo atpažintas", + "person_selected": "Asmuo pasirinktas", "photo_shared_all_users": "Panašu, kad savo nuotraukomis pasidalijote su visais naudotojais arba neturite naudotojų, su kuriais galėtumėte jomis pasidalyti.", "photos": "Nuotraukos", "photos_and_videos": "Nuotraukos ir vaizdo įrašai", "photos_count": "{count, plural, one {{count, number} nuotrauka} few {{count, number} nuotraukos} other {{count, number} nuotraukų}}", "photos_from_previous_years": "Ankstesnių metų nuotraukos", - "pick_a_location": "Išsirinkite vietovę", + "photos_only": "Tik nuotraukos", + "pick_a_location": "Pasirinkite vietovę", + "pick_custom_range": "Pasirinktinis diapazonas", + "pick_date_range": "Pasirinkti datos diapazoną", "pin_code_changed_successfully": "PIN kodas pakeistas sėkmingai", "pin_code_reset_successfully": "PIN kodas sėkmingai atstatytas", "pin_code_setup_successfully": "PIN kodas sėkmingai nustatytas", @@ -1654,6 +1703,9 @@ "play_memories": "Leisti atsiminimus", "play_motion_photo": "Rodyti judančias nuotraukas", "play_or_pause_video": "Rodyti arba sustabdyti vaizdo įrašą", + "play_original_video": "Rodyti originalų vaizdo įrašą", + "play_original_video_setting_description": "Rodyti originalius vaizdo įrašus vietoje konvertuotų vaizdo įrašų. Jei originalus įrašas yra nesuderinamas, jis gali groti neteisingai.", + "play_transcoded_video": "Rodyti konvertuotą vaizdo įrašą", "please_auth_to_access": "Prašome patvirtinti prisijungimą", "port": "Portas", "preferences_settings_subtitle": "Tvarkyti programos nuostatas", @@ -1677,7 +1729,7 @@ "profile_image_of_user": "{user} profilio nuotrauka", "profile_picture_set": "Profilio nuotrauka nustatyta.", "public_album": "Viešas albumas", - "public_share": "Viešas dilinimasis", + "public_share": "Viešas dalinimasis", "purchase_account_info": "Rėmėjas", "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", "purchase_activated_time": "Suaktyvinta {date}", @@ -1687,17 +1739,17 @@ "purchase_button_buy_immich": "Pirkti Immich", "purchase_button_never_show_again": "Niekada daugiau nerodyti", "purchase_button_reminder": "Priminti man po 30 dienų", - "purchase_button_remove_key": "Pašalinti produkto rakta", + "purchase_button_remove_key": "Pašalinti produkto raktą", "purchase_button_select": "Pasirinkti", "purchase_failed_activation": "Nepavyko suaktyvinti! Patikrinkite el. paštą, ar turite teisingo produkto koda!", "purchase_individual_description_1": "Asmeniui", "purchase_individual_description_2": "Rėmėjo statusas", "purchase_individual_title": "Asmeninis", "purchase_input_suggestion": "Turite produkto raktą? Įveskite jį žemiau", - "purchase_license_subtitle": "Įsigykite „Immich“, kad palaikytumėte tolesnį paslaugos vystymą", + "purchase_license_subtitle": "Įsigykite Immich, kad palaikytumėte tolesnį paslaugos vystymą", "purchase_lifetime_description": "Pirkimas visam gyvenimui", "purchase_option_title": "PIRKIMO PASIRINKIMAS", - "purchase_panel_info_1": "„Immich“ kūrimas užima daug laiko ir pastangų, o visą darbo dieną dirba inžinieriai, kad jis būtų kuo geresnis. Mūsų misija yra, kad atvirojo kodo programinė įranga ir etiška verslo praktika taptų tvariu kūrėjų pajamų šaltiniu ir sukurtų privatumą gerbiančią ekosistemą su realiomis alternatyvomis išnaudojamoms debesijos paslaugoms.", + "purchase_panel_info_1": "Immich kūrimas užima daug laiko ir pastangų, o visą darbo dieną dirba inžinieriai, kad jis būtų kuo geresnis. Mūsų misija yra, kad atvirojo kodo programinė įranga ir etiška verslo praktika taptų tvariu kūrėjų pajamų šaltiniu ir sukurtų privatumą gerbiančią ekosistemą su realiomis alternatyvomis išnaudojamoms debesijos paslaugoms.", "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamų sienų, šis pirkinys nesuteiks jums jokių papildomų Immich funkcijų. Mes tikime, kad tokie naudotojai kaip jūs palaikys nuolatinį Immich vystymąsi.", "purchase_panel_title": "Palaikykite projektą", "purchase_per_server": "Vienam serveriui", @@ -1710,12 +1762,18 @@ "purchase_server_description_2": "Rėmėjo statusas", "purchase_server_title": "Serveris", "purchase_settings_server_activated": "Serverio produkto raktas yra tvarkomas administratoriaus", + "query_asset_id": "Užklausti Elemento ID", + "queue_status": "Eilėje {count}/{total}", + "rate_asset": "Įvertinti Elementą", "rating": "Įvertinimas žvaigždutėmis", + "rating_clear": "Pašalinti įvertinimą", "rating_count": "{count, plural, one {# įvertinimas} few {# įvertinimai} other {# įvertinimų}}", "rating_description": "Rodyti EXIF įvertinimus informacijos skydelyje", + "reaction_options": "Reakcijų parinktys", "read_changelog": "Skaityti pakeitimų sąrašą", "ready_for_upload": "Paruošta įkėlimui", - "recent-albums": "Naujausi albumai", + "recent": "Naujausi", + "recent_albums": "Naujausi albumai", "recent_searches": "Naujausios paieškos", "recently_added": "Neseniai pridėta", "recently_added_page_title": "Neseniai pridėta", @@ -1731,17 +1789,24 @@ "refreshing_encoded_video": "Perkraunamas apdorotas vaizdo įrašas", "refreshing_faces": "Perkraunami veidai", "refreshing_metadata": "Perkraunami metaduomenys", + "regenerating_thumbnails": "Perkūriama miniatiūra", + "remote": "Nuotolinis", + "remote_assets": "Nuotoliniai Elementai", + "remote_media_summary": "Nuotolinės Medijos Santrauka", "remove": "Pašalinti", + "remove_assets_album_confirmation": "Ar tikrai norite pašalinti {count, plural, one {# elementą} few {# elementus} other {# elementų}} iš albumo?", "remove_assets_shared_link_confirmation": "Ar tikrai norite pašalinti {count, plural, one {# elementą} few {# elementus} other {# elementų}} iš šios bendrinimo nuorodos?", "remove_assets_title": "Pašalinti elementus?", "remove_deleted_assets": "Pašalinti Ištrintus Elemenuts", "remove_from_album": "Pašalinti iš albumo", "remove_from_album_action_prompt": "{count} pašalinta iš albumo", "remove_from_favorites": "Pašalinti iš mėgstamiausių", - "remove_from_lock_folder_action_prompt": "{count} ištraukta iš užrakinto aplanko", + "remove_from_lock_folder_action_prompt": "{count} pašalinta iš užrakinto aplanko", "remove_from_locked_folder": "Išimti iš užrakinto aplanko", "remove_from_locked_folder_confirmation": "Ar tikrai norite perkelti šias nuotraukas ir vaizdo įrašus iš užrakinto aplanko? Jie taps matomi jūsų galerijoje.", "remove_from_shared_link": "Pašalinti iš bendrinimo nuorodos", + "remove_memory": "Pašalinti atsiminimus", + "remove_photo_from_memory": "Pašalinti nuotrauką iš atsiminimų", "remove_tag": "Pašalinti žymę", "remove_url": "Pašalinti URL", "remove_user": "Pašalinti naudotoją", @@ -1758,21 +1823,36 @@ "replace_with_upload": "Pakeisti naujai įkeltu failu", "repository": "Repozitoriumas", "require_password": "Reikalauti slaptažodžio", + "require_user_to_change_password_on_first_login": "Reikalauti, kad vartotojas pakeistų slaptažodį pirmą kartą prisijungdamas", "rescan": "Perskenuoti", "reset": "Atstatyti", - "reset_password": "Atstayti slaptažodį", + "reset_password": "Atstatyti slaptažodį", + "reset_people_visibility": "Atstatyti žmonių matomumą", "reset_pin_code": "Atsatyti PIN kodą", "reset_pin_code_description": "Jei pamiršote PIN kodą, galite susisiekti su serverio administratoriumi, kad jis jį atstatytų", + "reset_pin_code_success": "Sėkmingai atstatytas PIN kodas", "reset_pin_code_with_password": "PIN kodą visada galite atkurti naudodami savo slaptažodį", + "reset_sqlite": "Atstatyti SQLite duomenų bazę", + "reset_sqlite_confirmation": "Ar tikrai norite atstatyti SQLite duomenų bazę? Turėsite atsijungti ir vėl prisijungti, kad iš naujo sinchronizuotumėte duomenis", + "reset_sqlite_success": "Sėkmingai atstatyta SQLite duomenų bazė", "reset_to_default": "Atkurti numatytuosius", "resolution": "Rezoliucija", "resolve_duplicates": "Sutvarkyti dublikatus", "resolved_all_duplicates": "Sutvarkyti visi dublikatai", "restore": "Atkurti", "restore_all": "Atkurti visus", + "restore_trash_action_prompt": "{count} atstatyta iš šiukšliadėžės", "restore_user": "Atkurti naudotoją", "restored_asset": "Atkurti elementą", + "resume": "Tęsti", + "resume_paused_jobs": "Tęsti {count, plural, one {# pristabdytą darbą} other {# pristabdytus darbus}}", + "retry_upload": "Bandyti išsiųsti dar kartą", "review_duplicates": "Peržiūrėti dublikatus", + "review_large_files": "Peržiūrėti didelius failus", + "role": "Rolė", + "role_editor": "Redaktorius", + "role_viewer": "Stebėtojas", + "running": "Vykdoma", "save": "Išsaugoti", "save_to_gallery": "Išsaugoti galerijoje", "saved": "Išsaugota", @@ -1791,15 +1871,21 @@ "search_albums": "Ieškoti albumų", "search_by_context": "Ieškoti pagal kontekstą", "search_by_description": "Ieškoti pagal aprašymą", - "search_by_description_example": "Žygio diena Sapoje", + "search_by_description_example": "Ilga kelionė per kopas", "search_by_filename": "Ieškoti pagal failo pavadinimą arba plėtinį", "search_by_filename_example": "pvz. IMG_1234.JPG arba PNG", + "search_by_ocr": "Ieškoti pagal OCR", + "search_by_ocr_example": "Latte", + "search_camera_lens_model": "Ieškoti objektyvo modelio...", "search_camera_make": "Ieškoti pagal kameros gamintoją...", "search_camera_model": "Ieškoti kameros modelį...", "search_city": "Ieškoti miesto...", "search_country": "Ieškoti šalies...", + "search_filter_apply": "Filtruoti", "search_filter_camera_title": "Pasirinkti kameros tipą", "search_filter_date": "Data", + "search_filter_date_interval": "{start} iki {end}", + "search_filter_date_title": "Pasirinkti datos diapazoną", "search_filter_display_option_not_in_album": "Ne albume", "search_filter_display_options": "Rodymo Nustatymai", "search_filter_filename": "Ieškoti pagal failo pavadinimą", @@ -1807,9 +1893,20 @@ "search_filter_location_title": "Pasirinkti vietovę", "search_filter_media_type": "Medijos tipas", "search_filter_media_type_title": "Pasirinkti medijos tipą", + "search_filter_ocr": "Ieškoti pagal OCR", + "search_filter_people_title": "Pasirinkti asmenis", + "search_filter_star_rating": "Įvertinimas", + "search_for": "Ieškoti ko", + "search_for_existing_person": "Ieškoti įvardinto asmens", "search_no_more_result": "Nėra daugiau rezultatų", + "search_no_people": "Be asmenų", "search_no_people_named": "Nėra žmonių vardu „{name}“", + "search_no_result": "Rezultatų nerasta, pabandykite kitą paieškos terminą ar derinį", + "search_options": "Paieškos parinktys", "search_page_categories": "Kategorijos", + "search_page_motion_photos": "Judanti Foto", + "search_page_no_objects": "Objekto info nepasiekiama", + "search_page_no_places": "Vietovės info nepasiekiama", "search_page_screenshots": "Ekrano nuotraukos", "search_page_search_photos_videos": "Ieškokite nuotraukų ir vaizdo įrašų", "search_page_selfies": "Asmenukės", @@ -1822,66 +1919,115 @@ "search_rating": "Ieškoti pagal įvertinimą...", "search_result_page_new_search_hint": "Nauja Paieška", "search_settings": "Ieškoti nustatymų", + "search_state": "Ieškoti valstijos/apskrities...", + "search_suggestion_list_smart_search_hint_1": "Išmanioji paieška įjungta pagal numatytuosius nustatymus, metaduomenų paieškai naudokite sintaksę ", + "search_suggestion_list_smart_search_hint_2": "Paieška", "search_tags": "Ieškoti žymų...", "search_timezone": "Ieškoti laiko zonos...", "search_type": "Paieškos tipas", "search_your_photos": "Ieškoti nuotraukų", + "searching_locales": "Ieškoma vietovių...", + "second": "Sekundė", + "see_all_people": "Pamatyti visus asmenis", + "select": "Pasirinkti", + "select_album": "Rinktis albumą", + "select_album_cover": "Rinktis albumo viršelį", + "select_albums": "Rinktis albumus", + "select_all": "Pasirinkti visus", "select_all_duplicates": "Pasirinkti visus dublikatus", "select_all_in": "Pažymėti visus esančius {group}", "select_avatar_color": "Pasirinkti avataro spalvą", + "select_count": "{count, plural, one {Pasirinkti #} other {Pasirinkti #}}", + "select_cutoff_date": "Pasirinkite galutinę datą", "select_face": "Pasirinkti veidą", "select_featured_photo": "Pasirinkti rodomą nuotrauką", "select_from_computer": "Pasirinkti iš kompiuterio", "select_keep_all": "Visus pažymėti \"Palikti\"", "select_library_owner": "Pasirinkti bibliotekos savininką", "select_new_face": "Pasirinkti naują veidą", + "select_people": "Pasirinkti asmenis", + "select_person": "Pasirinkti asmenį", + "select_person_to_tag": "Pasirinkti asmenį žymai", "select_photos": "Pasirinkti nuotraukas", "select_trash_all": "Visus pažymėti \"Išmesti\"", + "select_user_for_sharing_page_err_album": "Nepavyko sukurti albumo", "selected": "Pasirinkta", "selected_count": "{count, plural, one {# pasirinktas} few {# pasirinkti} other {# pasirinktų}}", "selected_gps_coordinates": "Pasirinkti GPS Koordinates", "send_message": "Siųsti žinutę", "send_welcome_email": "Siųsti sveikinimo el. laišką", - "server_info_box_app_version": "Programėlės versija", + "server_endpoint": "Serverio Galinis Taškas", + "server_info_box_app_version": "Programos versija", "server_info_box_server_url": "Serverio URL", "server_offline": "Serveris nepasiekiamas", "server_online": "Serveris pasiekiamas", "server_privacy": "Serverio Privatumas", + "server_restarting_description": "Šis puslapis atsinaujins neužilgo.", + "server_restarting_title": "Serveris restartuoja", "server_stats": "Serverio statistika", + "server_update_available": "Yra Serverio atnaujinimas", "server_version": "Serverio versija", "set": "Nustatyti", + "set_as_album_cover": "Naudoti kaip albumo viršelį", + "set_as_featured_photo": "Naudoti foto asmens profiliui", "set_as_profile_picture": "Nustatyti kaip profilio nuotrauką", "set_date_of_birth": "Nustatyti gimimo datą", "set_profile_picture": "Nustatyti profilio nuotrauką", "set_slideshow_to_fullscreen": "Nustatyti skaidrių peržiūrą per visą ekraną", "set_stack_primary_asset": "Nustatyti kaip pagrindinį elementą", + "setting_image_viewer_help": "Detali peržiūra pirmiausia įkelia mažą miniatiūrą, tada įkelia vidutinio dydžio versiją (jei įjungta) ir galiausiai įkelia originalą (jei įjungta).", + "setting_image_viewer_original_subtitle": "Įjunkite, kad įkeltumėte originalų pilnos raiškos vaizdą (didelį!). Išjunkite, kad sumažintumėte duomenų naudojimą (tiek tinkle, tiek įrenginio talpykloje).", "setting_image_viewer_original_title": "Užkrauti originalią nuotrauką", + "setting_image_viewer_preview_subtitle": "Įjunkite, jei norite įkelti vidutinės raiškos vaizdą. Išjunkite, jei norite tiesiogiai įkelti originalą ar naudoti tik miniatiūrą.", "setting_image_viewer_preview_title": "Užkrauti peržiūros nuotrauką", "setting_image_viewer_title": "Nuotraukos", "setting_languages_apply": "Pritaikyti", "setting_languages_subtitle": "Pakeisti programos kalbą", "setting_notifications_notify_failures_grace_period": "Informuoti apie foninio atsarginio kopijavimo nesėkmes: {duration}", "setting_notifications_notify_hours": "{count} valandų", + "setting_notifications_notify_immediately": "nedelsiant", "setting_notifications_notify_minutes": "{count} minučių", "setting_notifications_notify_never": "niekada", "setting_notifications_notify_seconds": "{count} sekundžių", "setting_notifications_single_progress_subtitle": "Detali įkėlimo progreso informacija kiekvienam elementui", + "setting_notifications_single_progress_title": "Rodyti foninio atsarginio kopijavimo eigą", + "setting_notifications_subtitle": "Koreguoti pranešimų nuostatas", + "setting_notifications_total_progress_subtitle": "Bendra įkėlimo eiga (atlikta/viso elementų)", + "setting_notifications_total_progress_title": "Rodyti visą foninio atsarginio kopijavimo eigą", + "setting_video_viewer_auto_play_subtitle": "Automatiškai pradėti leisti vaizdo įrašus juos atidarius", + "setting_video_viewer_auto_play_title": "Groti video automatiškai", + "setting_video_viewer_looping_title": "Kartotinai", + "setting_video_viewer_original_video_subtitle": "Transliuojant vaizdo įrašą iš serverio, leisti originalą, net jei įrašas yra konvertuotas. Įrašas gali strigti. Vietoje pasiekiami vaizdo įrašai leidžiami originalia kokybe, nepaisant šio nustatymo.", + "setting_video_viewer_original_video_title": "Priversti originalų vaizdo įrašą", "settings": "Nustatymai", "settings_require_restart": "Prašome perkrauti Immich, siekiant pritaikyti šį nustatymą", "settings_saved": "Nustatymai išsaugoti", "setup_pin_code": "Nustatyti PIN kodą", "share": "Dalintis", + "share_action_prompt": "{count} dalinamasi", "share_add_photos": "Įtraukti nuotraukų", "share_assets_selected": "{count} pažymėta", "share_dialog_preparing": "Ruošiama...", "share_link": "Bendrinti nuorodą", "shared": "Bendrinami", + "shared_album_activities_input_disable": "Komentarai išjungti", + "shared_album_activity_remove_content": "Ar norite ištrinti šią veiklą?", + "shared_album_activity_remove_title": "Ištrinti veiklą", + "shared_album_section_people_action_error": "Klaida išeinant/šalinant iš albumo", + "shared_album_section_people_action_leave": "Pašalinti naudotoją iš albumo", + "shared_album_section_people_action_remove_user": "Pašalinti naudotoją iš albumo", + "shared_album_section_people_title": "ASMENYS", + "shared_by": "Bendrina", "shared_by_user": "Bendrina {user}", "shared_by_you": "Bendrinama jūsų", "shared_from_partner": "Nuotraukos iš {partner}", "shared_intent_upload_button_progress_text": "{current} / {total} Įkelta", + "shared_link_app_bar_title": "Dalinimosi Nuorodos", "shared_link_clipboard_copied_massage": "Nukopijuota į iškarpinę", "shared_link_clipboard_text": "Nuoroda: {link}\nSlaptažodis: {password}", + "shared_link_create_error": "Klaida kuriant bendrinimo nuorodą", + "shared_link_custom_url_description": "Pasiekite šią bendrinimo nuorodą naudodami tinkintą URL", + "shared_link_edit_description_hint": "Įveskite bendrinimo aprašymą", "shared_link_edit_expire_after_option_day": "1 diena", "shared_link_edit_expire_after_option_days": "{count} dienų", "shared_link_edit_expire_after_option_hour": "1 valanda", @@ -1890,26 +2036,37 @@ "shared_link_edit_expire_after_option_minutes": "{count} minučių", "shared_link_edit_expire_after_option_months": "{count} mėnesių", "shared_link_edit_expire_after_option_year": "{count} metų", + "shared_link_edit_password_hint": "Įveskite bendrinimo slaptažodį", + "shared_link_edit_submit_button": "Atnaujinti nuorodą", + "shared_link_error_server_url_fetch": "Nepavyksta gauti serverio url", "shared_link_expires_day": "Galiojimas baigsis už {count} dienos", "shared_link_expires_days": "Galiojimas baigsis už {count} dienų", "shared_link_expires_hour": "Galiojimas baigsis už {count} valandos", "shared_link_expires_hours": "Galiojimas baigsis už {count} valandų", "shared_link_expires_minute": "Galiojimas baigsis už {count} minutės", "shared_link_expires_minutes": "Galiojimas baigsis už {count} minučių", + "shared_link_expires_never": "Galiojimas baigiasi ∞", "shared_link_expires_second": "Galiojimas baigsis už {count} sekundės", "shared_link_expires_seconds": "Galiojimas baigsis už {count} sekundžių", + "shared_link_individual_shared": "Asmuo pasidalintas", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_manage_links": "Valdyti Bendrinimo nuorodas", "shared_link_options": "Bendrinimo nuorodos parametrai", + "shared_link_password_description": "Bendrinimo nuorodos prieigai reikalingas slaptažodis", "shared_links": "Bendrinimo nuorodos", + "shared_links_description": "Dalintis foto ir video su nuoroda", "shared_photos_and_videos_count": "{assetCount, plural, one {# bendrinama nuotrauka ir vaizdo įrašas} few {# bendrinamos nuotraukos ir vaizdo įrašai} other {# bendrinamų nuotraukų ir vaizdo įrašų}}", "shared_with_me": "Bendrinama su manimi", "shared_with_partner": "Pasidalinta su {partner}", "sharing": "Dalijimasis", "sharing_enter_password": "Norėdami peržiūrėti šį puslapį, įveskite slaptažodį.", "sharing_page_album": "Bendrinami albumai", + "sharing_page_description": "Kurkite bendrinamus albumus, kad galėtumėte dalintis foto ir video su žmonėmis savo tinkle.", "sharing_page_empty_list": "TUŠČIAS SĄRAŠAS", "sharing_sidebar_description": "Rodyti bendrinimo rodinio nuorodą šoninėje juostoje", "sharing_silver_appbar_create_shared_album": "Naujas bendrinamas albumas", "sharing_silver_appbar_share_partner": "Bendrinti su partneriu", + "shift_to_permanent_delete": "spauskite ⇧, kad visam laikui ištrintumėte elementą", "show_album_options": "Rodyti albumo parinktis", "show_albums": "Rodyti albumus", "show_all_people": "Rodyti visus asmenis", @@ -1923,11 +2080,17 @@ "show_metadata": "Rodyti metaduomenis", "show_or_hide_info": "Rodyti arba slėpti informaciją", "show_password": "Rodyti slaptažodį", + "show_person_options": "Rodyti asmens parinktis", "show_progress_bar": "Rodyti progreso juostą", + "show_schema": "Rodyti schemą", "show_search_options": "Rodyti paieškos parinktis", + "show_shared_links": "Rodyti bendrinamas nuorodas", "show_slideshow_transition": "Rodyti perėjimą tarp skaidrių", "show_supporter_badge": "Rėmėjo ženklelis", "show_supporter_badge_description": "Rodyti rėmėjo ženklelį", + "show_text_recognition": "Rodyti teksto atpažinimą", + "show_text_search_menu": "Rodyti teksto paieškos meniu", + "shuffle": "Išmaišyti", "sidebar": "Šoninė juosta", "sidebar_display_description": "Rodyti rodinio nuorodą šoninėje juostoje", "sign_out": "Atsijungti", @@ -1937,6 +2100,8 @@ "skip_to_folders": "Praleisti iki aplankų", "skip_to_tags": "Praleisti iki žymių", "slideshow": "Skaidrių peržiūra", + "slideshow_repeat": "Kartoti skaidres", + "slideshow_repeat_description": "Pradėti iš pradžių, kai skaidrės baigiasi", "slideshow_settings": "Skaidrių peržiūros nustatymai", "sort_albums_by": "Rikiuoti albumus pagal...", "sort_created": "Sukūrimo data", @@ -1958,7 +2123,7 @@ "start": "Pradėti", "start_date": "Pradžios data", "start_date_before_end_date": "Pradžios data turi būti ankstesnė už pabaigos datą", - "state": "Valstija", + "state": "Valstija/Apskritis", "status": "Statusas", "stop_casting": "Nutraukti transliavimą", "stop_motion_photo": "Sustabdyti Judančią Foto", @@ -1980,6 +2145,10 @@ "sync": "Sinchronizuoti", "sync_albums": "Sinchronizuoti albumus", "sync_albums_manual_subtitle": "Sinchronizuoti visus įkeltus vaizdo įrašus ir nuotraukas su pasirinktomis atsarginėmis kopijomis", + "sync_local": "Sinchronizuoti vietinį", + "sync_remote": "Sinchronizuoti nuotolinį", + "sync_status": "Sinchronizacijos būklė", + "sync_status_subtitle": "Žiūrėti ir valdyti sinchronizacijos systemą", "sync_upload_album_setting_subtitle": "Sukurti ir įkelti jūsų nuotraukas ir vaizdo įrašus į pasirinktus Immich albumus", "tag": "Žyma", "tag_assets": "Pažymėti", @@ -1990,28 +2159,50 @@ "tag_updated": "Atnaujinta žyma: {tag}", "tagged_assets": "Žyma pridėta prie {count, plural, one {# elemento} other {# elementų}}", "tags": "Žymos", + "tap_to_run_job": "Paspauskite, kad pradėti darbą", "template": "Šablonas", "text_recognition": "Teksto atpažinimas", "theme": "Tema", "theme_selection": "Temos pasirinkimas", "theme_selection_description": "Automatiškai nustatykite šviesią arba tamsią temą pagal naršyklės sistemos nustatymus", + "theme_setting_asset_list_storage_indicator_title": "Rodyti saugyklos indikatorių elementų plytelėse", "theme_setting_asset_list_tiles_per_row_title": "Elementų per eilutę ({count})", + "theme_setting_colorful_interface_subtitle": "Fono paviršiams užtepkite pagrindinę spalvą.", + "theme_setting_colorful_interface_title": "Spalvinga sąsaja", + "theme_setting_image_viewer_quality_subtitle": "Koreguoti detalių vaizdų peržiūros kokybę", + "theme_setting_image_viewer_quality_title": "Vaizdo peržiūros priemonės kokybė", + "theme_setting_primary_color_subtitle": "Pasirinkite spalvą pagrindiniams veiksmams ir akcentams.", "theme_setting_primary_color_title": "Pagrindinė spalva", "theme_setting_system_primary_color_title": "Naudoti sistemos spalvą", "theme_setting_system_theme_switch": "Automatinė (Naudoti sistemos nustatymus)", + "theme_setting_theme_subtitle": "Pasirinkite programos temos nustatymą", "theme_setting_three_stage_loading_subtitle": "Trijų etapų įkėlimas gali padidinti įkėlimo našumą, tačiau sukelia žymiai didesnę tinklo apkrovą", + "theme_setting_three_stage_loading_title": "Įjungti trijų etapų įkėlimą", + "then": "Tada", + "they_will_be_merged_together": "Jie bus sujungti kartu", + "third_party_resources": "Trečios Šalies Ištekliai", + "time": "Laikas", "time_based_memories": "Atsiminimai pagal laiką", + "time_based_memories_duration": "Kiekvieno vaizdo rodymo laikas sekundėmis.", "timeline": "Laiko skalė", "timezone": "Laiko juosta", "to_archive": "Archyvuoti", "to_change_password": "Pakeisti slaptažodį", "to_favorite": "Įtraukti prie mėgstamiausių", "to_login": "Prisijungti", + "to_multi_select": "pasirinkti kelis elementus", + "to_parent": "Persikelti į viršų", + "to_select": "į pasirinkimą", "to_trash": "Išmesti", + "toggle_settings": "Įjungti nustatymus", + "toggle_theme_description": "Įjungti temą", "total": "Viso", + "total_usage": "Viso naudojama", "trash": "Šiukšliadėžė", + "trash_action_prompt": "{count} išmesta į šiukšliadėžę", "trash_all": "Perkelti visus į šiukšliadėžę", "trash_count": "Perkelti {count, number} į šiukšliadėžę", + "trash_delete_asset": "Išmesti/Ištrinti elementą", "trash_emptied": "Išvalytos šiukšlės", "trash_no_results_message": "Į šiukšliadėžę perkeltos nuotraukos ir vaizdo įrašai bus rodomi čia.", "trash_page_delete_all": "Ištrinti Visus", @@ -2019,15 +2210,24 @@ "trash_page_info": "Šiukšliadėžės elementai bus galutinai ištrinti už {days} dienų", "trash_page_no_assets": "Nėra išmestų elementų", "trash_page_restore_all": "Atkurti Visus", + "trash_page_select_assets_btn": "Pasirinkti elementus", "trash_page_title": "Šiukšlių ({count})", "trashed_items_will_be_permanently_deleted_after": "Į šiukšliadėžę perkelti elementai bus visam laikui ištrinti po {days, plural, one {# dienos} other {# dienų}}.", + "trigger_asset_uploaded": "Elementas Išsiųstas", + "trigger_person_recognized": "Asmuo Atpažintas", + "troubleshoot": "Šalinti triktis", "type": "Tipas", "unable_to_change_pin_code": "Negalima pakeisti PIN kodo", + "unable_to_check_version": "Nepavyko patvirtinti programos/serverio versijos", "unarchive": "Išarchyvuoti", + "unarchive_action_prompt": "{count} pašalinta iš Archyvo", "unarchived_count": "{count, plural, other {# išarchyvuota}}", "unfavorite": "Pašalinti iš mėgstamiausių", + "unfavorite_action_prompt": "{count} pašalinta iš Mėgstamiausių", "unhide_person": "Nebeslėpti žmogaus", + "unknown": "Nežinoma", "unknown_country": "Nežinoma Šalis", + "unknown_date": "Nežinoma data", "unknown_year": "Nežinomi metai", "unlimited": "Neribota", "unlink_oauth": "Atsieti OAuth", @@ -2038,13 +2238,18 @@ "unsaved_change": "Neišsaugoti pakeitimai", "unselect_all": "Atšaukti visų pasirinkimą", "unselect_all_duplicates": "Atžymėti visus dublikatus", + "unselect_all_in": "Atžymėti viską {group}", "unstack": "Išgrupuoti", + "unstack_action_prompt": "{count} išgrupuota", "unstacked_assets_count": "{count, plural, one {Išgrupuotas # elementas} few {Išgrupuoti # elementai} other {Išgrupuota # elementų}}", + "untagged": "Nepažymėta", "up_next": "Seknatis", + "update_location_action_prompt": "Atnaujinti {count} {count, plural, one {pasirinkto elemento} few {pasirinktų elementų} other {pasirinktų elementų}} vietovę naudojant:", "updated_at": "Atnaujintas", "updated_password": "Slaptažodis atnaujintas", "upload": "Įkelti", "upload_concurrency": "Įkėlimo lygiagretumas", + "upload_details": "Įkėlimo Detalės", "upload_dialog_info": "Ar norite sukurti pasirinkto(-ų) turinio(-ų) atsarginę kopiją serveryje?", "upload_dialog_title": "Įkelti turinį", "upload_errors": "Įkėlimas įvyko su {count, plural, one {# klaida} few {# klaidomis} other {# klaidų}}, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", @@ -2055,10 +2260,11 @@ "upload_success": "Įkėlimas pavyko, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", "upload_to_immich": "Įkelti į Immich ({count})", "uploading": "Įkeliama", + "uploading_media": "Įkeliama medija", "url": "URL", "usage": "Naudojimas", "use_biometric": "Naudoti biometriją", - "use_current_connection": "naudoti dabartinį ryšį", + "use_current_connection": "Naudoti dabartinį ryšį", "user": "Naudotojas", "user_has_been_deleted": "Šis naudotojas buvo ištrintas.", "user_id": "Naudotojo ID", @@ -2067,14 +2273,17 @@ "user_pin_code_settings_description": "Tvarkykite savo PIN kodą", "user_privacy": "Vartotojo Privatumas", "user_purchase_settings": "Įsigyti", + "user_purchase_settings_description": "Tvarkyti savo pirkinį", "user_role_set": "Nustatyti {user}, kaip {role}", "user_usage_stats": "Paskyros naudojimo statistika", "user_usage_stats_description": "Žiūrėti paskyros naudojimo statistiką", "username": "Naudotojo vardas", "users": "Naudotojai", + "users_added_to_album_count": "Pridėta {count, plural, one {# naudotojas} few {# naudotojai} other {# naudotojų}} į albumą", "utilities": "Įrankiai", "validate": "Validuoti", "validate_endpoint_error": "Prašome įvesti galiojantį URL", + "validation_error": "Patvirtinimo klaida", "variables": "Kintamieji", "version": "Versija", "version_announcement_closing": "Tavo draugas, Alex", @@ -2086,6 +2295,7 @@ "video_hover_setting_description": "Atkurti vaizdo įrašo miniatiūrą, kai pelė užvedama ant elemento. Net ir išjungus, atkūrimą galima pradėti užvedus pelės žymeklį ant atkūrimo piktogramos.", "videos": "Video", "videos_count": "{count, plural, one {# vaizdo įrašas} few {# vaizdo įrašai} other {# vaizdo įrašų}}", + "videos_only": "Tik Video", "view": "Žiūrėti", "view_album": "Žiūrėti albumą", "view_all": "Peržiūrėti viską", @@ -2094,6 +2304,7 @@ "view_link": "Žiūrėti nuorodą", "view_links": "Žiūrėti nuorodas", "view_name": "Žiūrėti", + "view_next_asset": "Žiūrėti sekantį elementą", "view_qr_code": "Žiūrėti QR kodą", "view_similar_photos": "Žiūrėti panašias foto", "view_stack": "Peržiūrėti grupę", diff --git a/i18n/lv.json b/i18n/lv.json index d9d2d52c6b..f5c96d8bcb 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -185,6 +185,7 @@ "maintenance_delete_backup": "Dzēst rezerves kopiju", "maintenance_delete_backup_description": "Šis fails tiks neatgriezeniski dzēsts.", "maintenance_delete_error": "Neizdevās dzēst rezerves kopiju.", + "maintenance_restore_backup": "Atjaunot no rezerves kopijas", "maintenance_restore_backup_different_version": "Šī rezerves kopija tika izveidota ar citu Immich versiju!", "maintenance_restore_backup_unknown_version": "Nevarēja noteikt rezerves kopijas versiju.", "maintenance_restore_database_backup_description": "Atgrizties pie iepriekšējā datubāzes stāvokļa, izmantojot rezerves kopijas failu", @@ -628,6 +629,8 @@ "client_cert_import": "Importēt", "client_cert_import_success_msg": "Klienta sertifikāts ir importēts", "client_cert_invalid_msg": "Nederīgs sertifikāta fails vai nepareiza parole", + "client_cert_password_message": "Ievadi šī sertifikāta paroli", + "client_cert_password_title": "Sertifikāta parole", "client_cert_remove_msg": "Klienta sertifikāts ir noņemts", "client_cert_subtitle": "Atbalsta tikai PKCS12 (.p12, .pfx) formātu. Sertifikātu importēšana/noņemšana ir pieejama tikai pirms pieslēgšanās", "client_cert_title": "SSL klienta sertifikāts [EKSPERIMENTĀLS]", @@ -792,6 +795,8 @@ "editor": "Redaktors", "editor_close_without_save_prompt": "Izmaiņas netiks saglabātas", "editor_close_without_save_title": "Aizvērt redaktoru?", + "editor_discard_edits_confirm": "Atmest labojumus", + "editor_discard_edits_title": "Atmest labojumus?", "editor_flip_horizontal": "Apvērst horizontāli", "editor_flip_vertical": "Apvērst vertikāli", "editor_orientation": "Orientācija", @@ -901,6 +906,8 @@ "features_in_development": "Izstrādes stadijā esošas funkcijas", "features_setting_description": "Lietotnes funkciju pārvaldība", "file_name_or_extension": "Faila nosaukums vai paplašinājums", + "file_name_text": "Faila nosaukums", + "file_name_with_value": "Faila nosaukums: {file_name}", "filename": "Faila nosaukums", "filetype": "Faila tips", "filter": "Filtrēt", @@ -1096,6 +1103,7 @@ "maintenance_restore_library": "Atjaunot tavu bibliotēku", "maintenance_restore_library_confirm": "Ja tas izskatās pareizi, turpini rezerves kopijas atjaunošanu!", "maintenance_restore_library_description": "Atjauno datubāzi", + "maintenance_restore_library_folder_has_files": "{folder} satur {count} mapes", "maintenance_restore_library_folder_no_files": "{folder} trūkst faili!", "maintenance_restore_library_folder_pass": "lasāms un rakstāms", "maintenance_restore_library_folder_read_fail": "nav nolasāms", @@ -1227,7 +1235,6 @@ "not_available": "Nav pieejams", "not_in_any_album": "Nav nevienā albumā", "not_selected": "Nav izvēlēts", - "note_apply_storage_label_to_previously_uploaded assets": "Piezīme: Lai piemērotu glabātuves nosaukumu iepriekš augšupielādētiem failiem, izpildiet", "notes": "Piezīmes", "nothing_here_yet": "Šeit vēl nekā nav", "notification_permission_dialog_content": "Lai iespējotu paziņojumus, atveriet Iestatījumi un atlasiet Atļaut.", diff --git a/i18n/ml.json b/i18n/ml.json index 7fc4475bc5..9e93ce9fc6 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -1468,7 +1468,6 @@ "not_available": "ലഭ്യമല്ല", "not_in_any_album": "ഒരു ആൽബത്തിലുമില്ല", "not_selected": "തിരഞ്ഞെടുത്തിട്ടില്ല", - "note_apply_storage_label_to_previously_uploaded assets": "കുറിപ്പ്: മുമ്പ് അപ്‌ലോഡ് ചെയ്ത അസറ്റുകളിൽ സ്റ്റോറേജ് ലേബൽ പ്രയോഗിക്കാൻ, ഇത് പ്രവർത്തിപ്പിക്കുക", "notes": "കുറിപ്പുകൾ", "nothing_here_yet": "ഇവിടെ ഇതുവരെ ഒന്നുമില്ല", "notification_permission_dialog_content": "അറിയിപ്പുകൾ പ്രവർത്തനക്ഷമമാക്കാൻ, ക്രമീകരണങ്ങളിലേക്ക് പോയി 'അനുവദിക്കുക' തിരഞ്ഞെടുക്കുക.", @@ -1663,7 +1662,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# അസറ്റ്} other {# അസറ്റുകൾ}} ഒരു പുതിയ വ്യക്തിക്ക് വീണ്ടും നൽകി", "reassing_hint": "തിരഞ്ഞെടുത്ത അസറ്റുകൾ നിലവിലുള്ള ഒരു വ്യക്തിക്ക് നൽകുക", "recent": "സമീപകാലം", - "recent-albums": "സമീപകാല ആൽബങ്ങൾ", + "recent_albums": "സമീപകാല ആൽബങ്ങൾ", "recent_searches": "സമീപകാല തിരയലുകൾ", "recently_added": "അടുത്തിടെ ചേർത്തത്", "recently_added_page_title": "അടുത്തിടെ ചേർത്തത്", diff --git a/i18n/mr.json b/i18n/mr.json index be0c96f9e9..f31b080e37 100644 --- a/i18n/mr.json +++ b/i18n/mr.json @@ -1463,7 +1463,6 @@ "not_available": "उपलब्ध नाही", "not_in_any_album": "कोणत्याही अल्बममध्ये नाही", "not_selected": "निवडलेले नाही", - "note_apply_storage_label_to_previously_uploaded assets": "नोट: आधी अपलोड केलेल्या अॅसेट्सवर स्टोरेज लेबल लागू करण्यासाठी हा आदेश चालवा", "notes": "नोट्स", "nothing_here_yet": "इथे अजून काही नाही", "notification_permission_dialog_content": "सूचना सक्षम करण्यासाठी सेटिंग्जमध्ये जा आणि अनुमती द्या.", @@ -1658,7 +1657,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# आयटम} other {# आयटम}} नव्या व्यक्तीकडे पुन्हा नियुक्त केले", "reassing_hint": "निवडलेले आयटम विद्यमान व्यक्तीकडे नियुक्त करा", "recent": "अलीकडील", - "recent-albums": "अलीकडील अल्बम", + "recent_albums": "अलीकडील अल्बम", "recent_searches": "अलीकडील शोध", "recently_added": "नुकतेच जोडलेले", "recently_added_page_title": "नुकतेच जोडलेले", diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 03cc792718..564c3c0de9 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -576,7 +576,7 @@ "asset_not_found_on_device_ios": "Elementet ble ikke funnet på enheten. Hvis du bruker iCloud, kan elementet være utilgjengelig på grunn av en feilaktig fil er lagret i iCloud", "asset_not_found_on_icloud": "Elementet ble ikke funnet på iCloud. Elementet kan være utilgjengelig fordi det ligger en feilaktig fil i iCloud", "asset_offline": "Fil utilgjengelig", - "asset_offline_description": "Dette elementet er offline. Immich kan ikke aksessere dets lokasjon. Vennligst påse at elementet er tilgjengelig og skann så biblioteket på nytt.", + "asset_offline_description": "Dette elementet er offline. Immich kan ikke finne dets possisjon. Vennligst påse at elementet er tilgjengelig og skann så biblioteket på nytt.", "asset_restored_successfully": "Objekt(er) gjenopprettet", "asset_skipped": "Hoppet over", "asset_skipped_in_trash": "I papirkurven", @@ -1471,9 +1471,9 @@ "manage_your_oauth_connection": "Administrer tilkoblingen din med OAuth", "map": "Kart", "map_assets_in_bounds": "{count, plural, =0 {Ingen bilder i dette området} one {# photo} other {# photos}}", - "map_cannot_get_user_location": "Kunne ikke hente brukerlokasjon", + "map_cannot_get_user_location": "Kan ikke hente brukerens plassering", "map_location_dialog_yes": "Ja", - "map_location_picker_page_use_location": "Bruk denne lokasjonen", + "map_location_picker_page_use_location": "Bruk dette stedet", "map_location_service_disabled_content": "Lokasjonstjeneste må være aktivert for å vise elementer fra din nåværende lokasjon. Vil du aktivere det nå?", "map_location_service_disabled_title": "Lokasjonstjeneste deaktivert", "map_marker_for_images": "Kart makeringer for bilder tatt i {city}, {country}", @@ -1604,7 +1604,6 @@ "not_available": "Ikke tilgjengelig", "not_in_any_album": "Ikke i noe album", "not_selected": "Ikke valgt", - "note_apply_storage_label_to_previously_uploaded assets": "Merk: For å bruke lagringsetiketten på tidligere opplastede filer, kjør", "notes": "Notater", "nothing_here_yet": "Ingenting her enda", "notification_permission_dialog_content": "For å aktivere notifikasjoner, gå til Innstillinger og velg tillat.", @@ -1723,7 +1722,7 @@ "pin_code_setup_successfully": "Vellykket oppsett av PIN kode", "pin_verification": "PINkode verifikasjon", "place": "Sted", - "places": "Plasseringer", + "places": "Steder", "places_count": "{count, plural, one {{count, number} Sted} other {{count, number} Steder}}", "play": "Spill av", "play_memories": "Spill av minner", @@ -1806,7 +1805,7 @@ "reassigned_assets_to_new_person": "Flyttet {count, plural, one {# element} other {# elementer}} til en ny person", "reassing_hint": "Tilordne valgte eiendeler til en eksisterende person", "recent": "Nylig", - "recent-albums": "Nylige album", + "recent_albums": "Nylige album", "recent_searches": "Nylige søk", "recently_added": "Nylig lagt til", "recently_added_page_title": "Nylig oppført", diff --git a/i18n/nl.json b/i18n/nl.json index 6d4f780dd3..24197a15b8 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -311,7 +311,7 @@ "search_jobs": "Taak zoeken…", "send_welcome_email": "Stuur een welkomstmail", "server_external_domain_settings": "Extern domein", - "server_external_domain_settings_description": "Domein voor openbaar gedeelde links, inclusief http(s)://", + "server_external_domain_settings_description": "Domein voor externe links", "server_public_users": "Openbare gebruikerslijst", "server_public_users_description": "Alle gebruikers (met naam en e-mailadres) worden weergegeven wanneer een gebruiker wordt toegevoegd aan gedeelde albums. Wanneer uitgeschakeld, is de gebruikerslijst alleen beschikbaar voor beheerders.", "server_settings": "Serverinstellingen", @@ -782,6 +782,8 @@ "client_cert_import": "Importeren", "client_cert_import_success_msg": "Cliëntcertificaat is geïmporteerd", "client_cert_invalid_msg": "Ongeldig certificaatbestand of verkeerd wachtwoord", + "client_cert_password_message": "Voer het wachtwoord voor dit certificaat in", + "client_cert_password_title": "Certificaat wachtwoord", "client_cert_remove_msg": "Clientcertificaat is verwijderd", "client_cert_subtitle": "Ondersteunt alleen PKCS12-formaat (.p12, .pfx). Het importeren/verwijderen van certificaten is alleen beschikbaar vóór het inloggen", "client_cert_title": "SSL clientcertificaat [EXPERIMENTEEL]", @@ -791,7 +793,12 @@ "collapse_all": "Alles inklappen", "color": "Kleur", "color_theme": "Kleurenthema", - "command": "Opdracht", + "command": "Commando", + "command_palette_prompt": "Vind snel pagina's, acties of commando's", + "command_palette_to_close": "om te sluiten", + "command_palette_to_navigate": "om te navigeren", + "command_palette_to_select": "om te selecteren", + "command_palette_to_show_all": "om alles te tonen", "comment_deleted": "Opmerking verwijderd", "comment_options": "Opties voor opmerkingen", "comments_and_likes": "Opmerkingen & likes", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "De wijzigingen worden niet opgeslagen", "editor_close_without_save_title": "Editor sluiten?", "editor_confirm_reset_all_changes": "Weet u zeker dat u alle wijzigingen wilt resetten?", + "editor_discard_edits_confirm": "Wijzigingen verwijderen", + "editor_discard_edits_prompt": "U heeft wijzigingen aangebracht die nog niet zijn opgeslagen. Weet u zeker dat u deze wilt verwijderen?", + "editor_discard_edits_title": "Wijzigingen verwijderen?", + "editor_edits_applied_error": "Het toepassen van de wijzigingen is mislukt", + "editor_edits_applied_success": "De wijzigingen zijn succesvol toegepast", "editor_flip_horizontal": "Horizontaal spiegelen", "editor_flip_vertical": "Verticaal spiegelen", "editor_orientation": "Oriëntatie", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "MENSEN", "exif_bottom_sheet_person_add_person": "Naam toevoegen", "exit_slideshow": "Diavoorstelling sluiten", + "expand": "Uitklappen", "expand_all": "Alles uitvouwen", "experimental_settings_new_asset_list_subtitle": "Werk in uitvoering", "experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen", @@ -1196,6 +1209,8 @@ "features_in_development": "Functies in ontwikkeling", "features_setting_description": "Beheer de app functies", "file_name_or_extension": "Bestandsnaam of extensie", + "file_name_text": "Bestandsnaam", + "file_name_with_value": "Bestandsnaam: {file_name}", "file_size": "Bestandsgrootte", "filename": "Bestandsnaam", "filetype": "Bestandstype", @@ -1604,7 +1619,6 @@ "not_available": "n.v.t.", "not_in_any_album": "Niet in een album", "not_selected": "Niet geselecteerd", - "note_apply_storage_label_to_previously_uploaded assets": "Opmerking: om het opslaglabel toe te passen op eerder geüploade items, voer de volgende taak uit", "notes": "Opmerkingen", "nothing_here_yet": "Hier staan nog geen items", "notification_permission_dialog_content": "Om meldingen in te schakelen, ga naar Instellingen en selecteer toestaan.", @@ -1634,6 +1648,7 @@ "online": "Online", "only_favorites": "Alleen favorieten", "open": "Openen", + "open_calendar": "Open kalender", "open_in_map_view": "Openen in kaartweergave", "open_in_openstreetmap": "Openen in OpenStreetMap", "open_the_search_filters": "Open de zoekfilters", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan een nieuw persoon", "reassing_hint": "Geselecteerde items toewijzen aan een bestaand persoon", "recent": "Recent", - "recent-albums": "Recente albums", + "recent_albums": "Recente albums", "recent_searches": "Recente zoekopdrachten", "recently_added": "Onlangs toegevoegd", "recently_added_page_title": "Recent toegevoegd", @@ -2175,6 +2190,7 @@ "support": "Ondersteuning", "support_and_feedback": "Ondersteuning & feedback", "support_third_party_description": "Je Immich installatie is door een derde partij samengesteld. Problemen die je ervaart, kunnen door dat pakket veroorzaakt zijn. Meld problemen in eerste instantie bij hen via de onderstaande links.", + "supporter": "Supporter", "swap_merge_direction": "Wissel richting voor samenvoegen om", "sync": "Synchroniseren", "sync_albums": "Albums synchroniseren", @@ -2286,7 +2302,7 @@ "unstack_action_prompt": "{count} item(s) ontstapeld", "unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld", "unsupported_field_type": "Veldtype niet ondersteund", - "untagged": "Ongemarkeerd", + "untagged": "Zonder tags", "untitled_workflow": "Naamloze werkstroom", "up_next": "Volgende", "update_location_action_prompt": "Werk de locatie bij van {count} geselecteerde items met:", diff --git a/i18n/nn.json b/i18n/nn.json index 73a9d02c14..cbf81e4807 100644 --- a/i18n/nn.json +++ b/i18n/nn.json @@ -5,8 +5,10 @@ "acknowledge": "Merk som lese", "action": "Handling", "action_common_update": "Oppdater", + "action_description": "Eit sett med handlingar som skal utføras på dei filtrerte ressursane", "actions": "Handlingar", "active": "Aktive", + "active_count": "Aktive: {count}", "activity": "Aktivitet", "activity_changed": "Aktivitet er {enabled, select, true {aktivert} other {deaktivert}}", "add": "Legg til", @@ -14,9 +16,14 @@ "add_a_location": "Legg til ein stad", "add_a_name": "Legg til eit namn", "add_a_title": "Legg til ein tittel", + "add_action": "Legg til handling", + "add_action_description": "Trykk for å leggja til ei handling som skal utføras", + "add_assets": "Legg til ressursar", "add_birthday": "Legg til ein fødselsdag", "add_endpoint": "Legg til endepunkt", "add_exclusion_pattern": "Legg til unnlatingsmønster", + "add_filter": "Legg til filter", + "add_filter_description": "Trykk for å leggja til eit filtervilkår", "add_location": "Legg til stad", "add_more_users": "Legg til fleire brukarar", "add_partner": "Legg til partnar", @@ -27,6 +34,7 @@ "add_to_album": "Legg til i album", "add_to_album_bottom_sheet_added": "Lagt til i {album}", "add_to_album_bottom_sheet_already_exists": "Allereie i {album}", + "add_to_album_bottom_sheet_some_local_assets": "Somme lokale eigedelar kunne ikkje leggjast til i album", "add_to_albums": "Legg til i album", "add_to_albums_count": "Legg til i album ({count})", "add_to_shared_album": "Legg til i delt album", diff --git a/i18n/package.json b/i18n/package.json index cb3560bea1..47748c28e8 100644 --- a/i18n/package.json +++ b/i18n/package.json @@ -1,6 +1,6 @@ { "name": "immich-i18n", - "version": "2.5.5", + "version": "2.5.6", "private": true, "scripts": { "format": "prettier --check .", diff --git a/i18n/pl.json b/i18n/pl.json index 37ea94d2c9..d98533a41e 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -782,6 +782,8 @@ "client_cert_import": "Importuj", "client_cert_import_success_msg": "Certyfikat klienta został zaimportowany", "client_cert_invalid_msg": "Nieprawidłowy plik certyfikatu lub nieprawidłowe hasło", + "client_cert_password_message": "Wprowadź hasło dla tego certyfikatu", + "client_cert_password_title": "Hasło certyfikatu", "client_cert_remove_msg": "Certyfikat klienta został usunięty", "client_cert_subtitle": "Obsługuje wyłącznie format PKCS12 (.p12, .pfx). Importowanie/usuwanie certyfikatów jest dostępne wyłącznie przed zalogowaniem", "client_cert_title": "Certyfikat klienta SSL [EKSPERYMENTALNE]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "Zmiany nie zostaną zapisane", "editor_close_without_save_title": "Zamknąć edytor?", "editor_confirm_reset_all_changes": "Czy na pewno chcesz zresetować wszystkie zmiany?", + "editor_discard_edits_confirm": "Odrzuć zmiany", + "editor_discard_edits_prompt": "Masz niezapisane zmiany. Czy na pewno chcesz je odrzucić?", + "editor_discard_edits_title": "Odrzucić zmiany?", + "editor_edits_applied_error": "Nie udało się zastosować zmian", + "editor_edits_applied_success": "Zmiany zostały pomyślnie zastosowane", "editor_flip_horizontal": "Odwróć poziomo", "editor_flip_vertical": "Odwróć pionowo", "editor_orientation": "Orientacja", @@ -1196,6 +1203,8 @@ "features_in_development": "Funkcje w fazie rozwoju", "features_setting_description": "Zarządzaj funkcjami aplikacji", "file_name_or_extension": "Nazwie lub rozszerzeniu pliku", + "file_name_text": "Nazwa pliku", + "file_name_with_value": "Nazwa pliku: {file_name}", "file_size": "Rozmiar pliku", "filename": "Nazwa pliku", "filetype": "Typ pliku", @@ -1604,7 +1613,6 @@ "not_available": "Nie dotyczy", "not_in_any_album": "Bez albumu", "not_selected": "Nie wybrano", - "note_apply_storage_label_to_previously_uploaded assets": "Uwaga: Aby przypisać etykietę magazynowania do wcześniej przesłanych zasobów, uruchom", "notes": "Uwagi", "nothing_here_yet": "Nic tu jeszcze nie ma", "notification_permission_dialog_content": "Aby włączyć powiadomienia, przejdź do Ustawień i wybierz opcję Zezwalaj.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "Przypisano ponownie {count, plural, one {# zasób} other {# zasobów}} do nowej osoby", "reassing_hint": "Przypisz wybrane zasoby do istniejącej osoby", "recent": "Ostatnie", - "recent-albums": "Ostatnie albumy", + "recent_albums": "Ostatnie albumy", "recent_searches": "Ostatnie wyszukiwania", "recently_added": "Ostatnio dodane", "recently_added_page_title": "Ostatnio Dodane", @@ -2056,7 +2064,7 @@ "shared_by_you": "Udostępnione przez ciebie", "shared_from_partner": "Zdjęcia od {partner}", "shared_intent_upload_button_progress_text": "{current} / {total} Przesłano", - "shared_link_app_bar_title": "Udostępnione linki", + "shared_link_app_bar_title": "Udostępnione", "shared_link_clipboard_copied_massage": "Skopiowane do schowka", "shared_link_clipboard_text": "Link: {link}\nHasło: {password}", "shared_link_create_error": "Błąd podczas tworzenia linka do udostępnienia", diff --git a/i18n/pt.json b/i18n/pt.json index fdf3613d1e..4d281c94fa 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -782,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado do cliente foi importado", "client_cert_invalid_msg": "Certificado inválido ou palavra-passe incorreta", + "client_cert_password_message": "Insira a palavra-passe para este certificado", + "client_cert_password_title": "Palavra-passe do Certificado", "client_cert_remove_msg": "Certificado do cliente foi removido", "client_cert_subtitle": "Apenas há suporte ao formato PKCS12 (.p12, .pfx). Importar/Remover certificados está disponível apenas antes do início de sessão", "client_cert_title": "Certificado de Cliente SSL [EXPERIMENTAL]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "As alterações não serão guardadas", "editor_close_without_save_title": "Fechar editor?", "editor_confirm_reset_all_changes": "Tem a certeza de que quer desfazer todas as alterações?", + "editor_discard_edits_confirm": "Descartar Alterações", + "editor_discard_edits_prompt": "Tem alterações não guardadas. Tem a certeza de que quer descartá-las?", + "editor_discard_edits_title": "Descartar alterações?", + "editor_edits_applied_error": "Não foi possível aplicar alterações", + "editor_edits_applied_success": "Alterações aplicadas com sucesso", "editor_flip_horizontal": "Espelhar na horizontal", "editor_flip_vertical": "Espelhar na vertical", "editor_orientation": "Orientação", @@ -1196,6 +1203,8 @@ "features_in_development": "Funcionalidades em Desenvolvimento", "features_setting_description": "Configurar as funcionalidades da aplicação", "file_name_or_extension": "Nome do ficheiro ou extensão", + "file_name_text": "Nome do ficheiro", + "file_name_with_value": "Nome do ficheiro: {file_name}", "file_size": "Tamanho do ficheiro", "filename": "Nome do ficheiro", "filetype": "Tipo de ficheiro", @@ -1604,7 +1613,6 @@ "not_available": "N/A", "not_in_any_album": "Não está em nenhum álbum", "not_selected": "Não selecionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o Rótulo de Armazenamento a ficheiros carregados anteriormente, execute o", "notes": "Notas", "nothing_here_yet": "Ainda não existe nada aqui", "notification_permission_dialog_content": "Para ativar as notificações, vá em Configurações e selecione permitir.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "Reatribuído {count, plural, one {# ficheiro} other {# ficheiros}} a uma nova pessoa", "reassing_hint": "Atribuir ficheiros selecionados a uma pessoa existente", "recent": "Recentes", - "recent-albums": "Álbuns recentes", + "recent_albums": "Álbuns recentes", "recent_searches": "Pesquisas recentes", "recently_added": "Adicionados Recentemente", "recently_added_page_title": "Adicionado recentemente", diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index 6a98ddf8e1..7f2845fc53 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -782,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado do cliente importado", "client_cert_invalid_msg": "Arquivo de certificado inválido ou senha errada", + "client_cert_password_message": "Entre com a senha para esse certificado", + "client_cert_password_title": "Senha do certificado", "client_cert_remove_msg": "Certificado do cliente removido", "client_cert_subtitle": "Suporta apenas o formato PKCS12 (.p12, .pfx). A importação/remoção de certificados está disponível apenas antes do login", "client_cert_title": "Certificado de cliente SSL [EXPERIMENTAL]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "As alterações não serão salvas", "editor_close_without_save_title": "Fechar editor?", "editor_confirm_reset_all_changes": "Tem certeza que deseja desfazer todas alterações?", + "editor_discard_edits_confirm": "Descartar alterações", + "editor_discard_edits_prompt": "Você possui alterações que não foram salvas. Tem certeza que deseja descarta-las?", + "editor_discard_edits_title": "Desfazer as alterações?", + "editor_edits_applied_error": "Falhou ao salvar as alterações", + "editor_edits_applied_success": "Alterações salvas com sucesso", "editor_flip_horizontal": "Virar na horizontal", "editor_flip_vertical": "Virar na vertical", "editor_orientation": "Orientação", @@ -1196,6 +1203,8 @@ "features_in_development": "Funções em desenvolvimento", "features_setting_description": "Gerenciar as funcionalidades da aplicação", "file_name_or_extension": "Nome do arquivo ou extensão", + "file_name_text": "Nome do arquivo", + "file_name_with_value": "Nome do arquivo: {file_name}", "file_size": "Tamanho do arquivo", "filename": "Nome do arquivo", "filetype": "Tipo de arquivo", @@ -1604,7 +1613,6 @@ "not_available": "N/A", "not_in_any_album": "Fora de álbum", "not_selected": "Não selecionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos enviados anteriormente, execute o", "notes": "Notas", "nothing_here_yet": "Ainda não existe nada aqui", "notification_permission_dialog_content": "Para ativar as notificações, vá em Configurações e selecione permitir.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a uma nova pessoa", "reassing_hint": "Atribuir arquivos selecionados a uma pessoa existente", "recent": "Recente", - "recent-albums": "Álbuns recentes", + "recent_albums": "Álbuns recentes", "recent_searches": "Pesquisas recentes", "recently_added": "Adicionado recentemente", "recently_added_page_title": "Adicionados recentemente", diff --git a/i18n/ro.json b/i18n/ro.json index b9b04b7cce..b36ae22d36 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -272,7 +272,7 @@ "oauth_auto_register": "Auto înregistrare", "oauth_auto_register_description": "Înregistrează automat utilizatori noi după autentificarea cu OAuth", "oauth_button_text": "Text buton", - "oauth_client_secret_description": "Necesar dacă PKCE (Proof Key for Code Exchange) nu este suportat de furnizorul OAuth", + "oauth_client_secret_description": "Necesar pentru un client confidențial sau dacă PKCE (Proof Key for Code Exchange) nu este suportat pentru un client public.", "oauth_enable_description": "Autentifică-te cu OAuth", "oauth_mobile_redirect_uri": "URI de redirecționare mobilă", "oauth_mobile_redirect_uri_override": "Înlocuire URI de redirecționare mobilă", @@ -513,7 +513,7 @@ "albums_default_sort_order_description": "Ordinea inițială de sortare a pozelor la crearea de albume noi.", "albums_feature_description": "Colecții de date care pot fi partajate cu alți utilizatori.", "albums_on_device_count": "{count} albume pe dispozitiv", - "albums_selected": "{număra, plural, unul {# album selectat} altele {# albumuri selectate}}", + "albums_selected": "{număr, plural, unul {# album selectat} altele {# albumuri selectate}}", "all": "Toate", "all_albums": "Toate albumele", "all_people": "Toți oamenii", @@ -626,7 +626,7 @@ "backup_album_selection_page_select_albums": "Selectează albume", "backup_album_selection_page_selection_info": "Informații selecție", "backup_album_selection_page_total_assets": "Total resurse unice", - "backup_albums_sync": "Sincronizarea albumelor de backup", + "backup_albums_sync": "Sincronizarea albumelor de rezervă", "backup_all": "Toate", "backup_background_service_backup_failed_message": "Eșuare backup resurse. Reîncercare…", "backup_background_service_complete_notification": "Backup resurse finalizat", @@ -766,9 +766,9 @@ "cleanup_found_assets": "Am găsit {count} materiale in copia de rezerva", "cleanup_found_assets_with_size": "{count} obiecte găsite ({size})", "cleanup_icloud_shared_albums_excluded": "Albumele partajate iCLoud sunt excluse de la cautare", - "cleanup_no_assets_found": "Nici un material in copia de rezerva găsit după criteriu", + "cleanup_no_assets_found": "Nu au fost găsite fișiere care să corespundă criteriilor de mai sus. „Eliberare spațiu” poate șterge doar fișierele care au fost deja salvate pe server.", "cleanup_preview_title": "Materiale sa fie șterse ({count})", - "cleanup_step3_description": "Scanați pentru fotografii și videoclipuri pentru care au fost făcute copii de rezervă pe server cu data limită selectată și opțiunile de filtrare", + "cleanup_step3_description": "Scanează fișierele salvate pe server care corespund setărilor tale de dată și păstrare.", "cleanup_step4_summary": "{count} elemente create înainte de {date} sunt puse în coadă pentru a fi eliminate de pe dispozitiv", "cleanup_trash_hint": "Pentru a recupera complet spațiu de stocare, deschideți aplicația Galerie și goliți coșul de gunoi", "clear": "Curățați", @@ -782,6 +782,8 @@ "client_cert_import": "Importă", "client_cert_import_success_msg": "Certificatul de client este importat", "client_cert_invalid_msg": "Fisier cu certificat invalid sau parola este greșită", + "client_cert_password_message": "Introduceți parola pentru acest certificat", + "client_cert_password_title": "Parola certificatului", "client_cert_remove_msg": "Certificatul de client este șters", "client_cert_subtitle": "Este suportat doar formatul PKCS12 (.p12, .pfx). Importul/ștergerea certificatului este disponibil(ă) doar înainte de autentificare", "client_cert_title": "Certificat SSL pentru client [EXPERIMENTAL]", @@ -867,8 +869,8 @@ "custom_locale": "Setare Regională Personalizată", "custom_locale_description": "Formatați datele și numerele în funcție de limbă și regiune", "custom_url": "URL personalizat", - "cutoff_date_description": "Eliminați fotografiile și videoclipurile mai vechi de", - "cutoff_day": "{count, plural, o {day} mai multe {days}}", + "cutoff_date_description": "Păstrează fotografiile din ultimele…", + "cutoff_day": "{număr, plural, o {day} mai multe {days}}", "cutoff_year": "{count, plural, =0 {0 ani} one {# an} few {# ani} other {# de ani}}", "daily_title_text_date": "E, LLL zz", "daily_title_text_date_year": "E, LLL zz, aaaa", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "Schimbările nu vor fi salvate", "editor_close_without_save_title": "Închideți editorul?", "editor_confirm_reset_all_changes": "Sigur vrei să resetezi toate modificările?", + "editor_discard_edits_confirm": "Renunță modificările", + "editor_discard_edits_prompt": "Ai modificări nesalvate. Ești sigur că vrei să le renunți?", + "editor_discard_edits_title": "Renunți la modificări?", + "editor_edits_applied_error": "Nu s-au putut aplica modificările", + "editor_edits_applied_success": "Modificările au fost aplicate cu succes", "editor_flip_horizontal": "Întoarceți orizontal", "editor_flip_vertical": "Întoarceți vertical", "editor_orientation": "Orientare", @@ -1196,6 +1203,8 @@ "features_in_development": "Funcții în dezvoltare", "features_setting_description": "Gestionați funcțiile aplicației", "file_name_or_extension": "Numele sau extensia fișierului", + "file_name_text": "Nume fișier", + "file_name_with_value": "Nume fișier: {file_name}", "file_size": "Mărime fișier", "filename": "Numele fișierului", "filetype": "Tipul fișierului", @@ -1214,7 +1223,7 @@ "forgot_pin_code_question": "Ai uitat codul PIN?", "forward": "Redirecționare", "free_up_space": "Eliberați spațiu", - "free_up_space_description": "Mută fotografiile și videoclipurile salvate în coșul de gunoi al dispozitivului pentru a elibera spațiu. Copiile tale de pe server rămân în siguranță", + "free_up_space_description": "Mută fotografiile și videoclipurile salvate în coșul de gunoi al dispozitivului pentru a elibera spațiu. Copiile tale de pe server rămân în siguranță.", "free_up_space_settings_subtitle": "Eliberați spațiul de stocare al dispozitivului", "full_path": "Calea completă: {path}", "gcast_enabled": "Google Cast", @@ -1574,7 +1583,7 @@ "no_albums_with_name_yet": "Se pare că nu aveți încă niciun album cu acest nume.", "no_albums_yet": "Se pare că nu aveți încă niciun album.", "no_archived_assets_message": "Arhivați fotografii și videoclipuri pentru a le ascunde din vizualizarea fotografii", - "no_assets_message": "CLICK PENTRU A ÎNCĂRCA PRIMA TA FOTOGRAFIE", + "no_assets_message": "Apasă pentru a încărca prima ta fotografie.", "no_assets_to_show": "Nicio resursă de afișat", "no_cast_devices_found": "Nu s-au găsit dispozitive de difuzare", "no_checksum_local": "Nu există checksum – nu se pot prelua resursele locale", @@ -1604,7 +1613,6 @@ "not_available": "N/A", "not_in_any_album": "Nu există în niciun album", "not_selected": "Neselectat", - "note_apply_storage_label_to_previously_uploaded assets": "Notă: Pentru a aplica eticheta de stocare la resursele încărcate anterior, rulați", "notes": "Note", "nothing_here_yet": "Nimic aici încă", "notification_permission_dialog_content": "Pentru a activa notificările, mergi în Setări > Immich și selectează permite.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "Re-alocat {count, plural, one {# resursă} other {# resurse}} unei noi persoane", "reassing_hint": "Atribuiți resursele selectate unei persoane existente", "recent": "Recent", - "recent-albums": "Albume recente", + "recent_albums": "Albume recente", "recent_searches": "Căutări recente", "recently_added": "Adăugate recent", "recently_added_page_title": "Adăugate recent", @@ -2251,7 +2259,7 @@ "trigger_asset_uploaded": "Fișier încărcat", "trigger_asset_uploaded_description": "Declanșează cand un fișier este încarcat", "trigger_description": "Un eveniment care declanșează fluxul de lucru", - "trigger_person_recognized": "Persoana Recunoscută", + "trigger_person_recognized": "Persoană Recunoscută", "trigger_person_recognized_description": "Declanșat atunci când este detectată o persoană", "trigger_type": "Tip de declanșare", "troubleshoot": "Depanați", @@ -2287,7 +2295,7 @@ "unstacked_assets_count": "Nestivuit {count, plural, one {# resursă} other {# resurse}}", "unsupported_field_type": "Tip de câmp neacceptat", "untagged": "Neetichetat", - "untitled_workflow": "Flux fara titlu", + "untitled_workflow": "Flux de lucru fără titlu", "up_next": "Mai departe", "update_location_action_prompt": "Actualizează locația pentru {count} resurse selectate cu:", "updated_at": "Actualizat", @@ -2297,7 +2305,7 @@ "upload_details": "Detalii încărcare", "upload_dialog_info": "Vrei să backup resursele selectate pe server?", "upload_dialog_title": "Încarcă resursă", - "upload_error_with_count": "Eroare la încărcare pentru {count, plural, one {# fișier} other {# fișiere}}", + "upload_error_with_count": "Eroare la încărcare pentru {număr, plural, un {# fișier} alte {# fișiere}}", "upload_errors": "Încărcare finalizată cu {count, plural, one {# eroare} other {# erori}}, reîmprospătați pagina pentru a reîncărca noile resurse.", "upload_finished": "Încărcarea s-a finalizat", "upload_progress": "Rămas {remaining, number} - Procesat {processed, number}/{total, number}", @@ -2312,7 +2320,7 @@ "url": "URL", "usage": "Utilizare", "use_biometric": "Folosește biometrice", - "use_current_connection": "folosește conexiunea curentă", + "use_current_connection": "Folosește conexiunea curentă", "use_custom_date_range": "Utilizați în schimb un interval de date personalizat", "user": "Utilizator", "user_has_been_deleted": "Acest utilizator a fost șters.", diff --git a/i18n/ru.json b/i18n/ru.json index ed94f6de71..a1e542a2e5 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -311,7 +311,7 @@ "search_jobs": "Поиск задач…", "send_welcome_email": "Отправить приветственное письмо", "server_external_domain_settings": "Внешний домен", - "server_external_domain_settings_description": "Домен для публичных ссылок, включая http(s)://", + "server_external_domain_settings_description": "Домен для публичных ссылок", "server_public_users": "Публичные пользователи", "server_public_users_description": "Выводить список пользователей (имена и email) в общих альбомах. Когда отключено, список доступен только администраторам, пользователи смогут делиться только ссылкой.", "server_settings": "Настройки сервера", @@ -782,6 +782,8 @@ "client_cert_import": "Импорт", "client_cert_import_success_msg": "Клиентский сертификат импортирован", "client_cert_invalid_msg": "Неверный файл сертификата или неверный пароль", + "client_cert_password_message": "Введите пароль для этого сертификата", + "client_cert_password_title": "Пароль от сертификата", "client_cert_remove_msg": "Клиентский сертификат удален", "client_cert_subtitle": "Поддерживается только формат PKCS12 (.p12, .pfx). Импорт/удаление сертификата доступно только перед входом в систему.", "client_cert_title": "[ЭКСПЕРИМЕНТАЛЬНО] Клиентский SSL-сертификат", @@ -792,6 +794,11 @@ "color": "Цвет", "color_theme": "Цветовая тема", "command": "Команда", + "command_palette_prompt": "Быстрый поиск страниц, действий или команд", + "command_palette_to_close": "закрыть", + "command_palette_to_navigate": "навигация", + "command_palette_to_select": "выбрать", + "command_palette_to_show_all": "показать все", "comment_deleted": "Комментарий удалён", "comment_options": "Действия с комментарием", "comments_and_likes": "Комментарии и отметки \"нравится\"", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "Изменения не будут сохранены", "editor_close_without_save_title": "Закрыть редактор?", "editor_confirm_reset_all_changes": "Отменить все сделанные изменения?", + "editor_discard_edits_confirm": "Отменить изменения", + "editor_discard_edits_prompt": "Вы действительно хотите отменить все не сохранённые изменения?", + "editor_discard_edits_title": "Отменить изменения?", + "editor_edits_applied_error": "Не удалось применить изменения", + "editor_edits_applied_success": "Изменения успешно применены", "editor_flip_horizontal": "Отразить горизонтально", "editor_flip_vertical": "Отразить вертикально", "editor_orientation": "Ориентация", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "ЛЮДИ", "exif_bottom_sheet_person_add_person": "Добавить имя", "exit_slideshow": "Выйти из слайд-шоу", + "expand": "Развернуть", "expand_all": "Развернуть всё", "experimental_settings_new_asset_list_subtitle": "В разработке", "experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий", @@ -1196,6 +1209,8 @@ "features_in_development": "Функции в разработке", "features_setting_description": "Управление дополнительными возможностями приложения", "file_name_or_extension": "Имя файла или расширение", + "file_name_text": "Имя файла", + "file_name_with_value": "Имя файла: {file_name}", "file_size": "Размер файла", "filename": "Имя файла", "filetype": "Тип файла", @@ -1604,7 +1619,6 @@ "not_available": "Нет данных", "not_in_any_album": "Ни в одном альбоме", "not_selected": "Не выбрано", - "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить метку хранилища к ранее загруженным объектам, запустите", "notes": "Примечание", "nothing_here_yet": "Здесь пока ничего нет", "notification_permission_dialog_content": "Чтобы включить уведомления, перейдите в «Настройки» и выберите «Разрешить».", @@ -1634,6 +1648,7 @@ "online": "Доступен", "only_favorites": "Только избранное", "open": "Открыть", + "open_calendar": "Открыть календарь", "open_in_map_view": "Открыть в режиме просмотра карты", "open_in_openstreetmap": "Открыть в OpenStreetMap", "open_the_search_filters": "Открыть фильтры поиска", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "Лица на {count, plural, one {# объекте} other {# объектах}} переназначены на нового человека", "reassing_hint": "Назначить выбранные объекты указанному человеку", "recent": "Недавние", - "recent-albums": "Недавние альбомы", + "recent_albums": "Недавние альбомы", "recent_searches": "Недавние поисковые запросы", "recently_added": "Недавно добавленные", "recently_added_page_title": "Недавно добавленные", @@ -2120,7 +2135,7 @@ "show_search_options": "Показать параметры поиска", "show_shared_links": "Показать публичные ссылки", "show_slideshow_transition": "Плавный переход", - "show_supporter_badge": "Значок поддержки", + "show_supporter_badge": "Значок спонсорства", "show_supporter_badge_description": "Показать значок поддержки", "show_text_recognition": "Показать распознанный текст", "show_text_search_menu": "Показать меню текстового поиска", @@ -2175,6 +2190,7 @@ "support": "Поддержка", "support_and_feedback": "Поддержка и обратная связь", "support_third_party_description": "Ваша установка immich была упакована сторонним разработчиком. Проблемы, с которыми вы столкнулись, могут быть вызваны этим пакетом, поэтому, пожалуйста, в первую очередь обращайтесь к ним, используя ссылки ниже.", + "supporter": "Спонсор Immich", "swap_merge_direction": "Изменить направление слияния", "sync": "Синхр.", "sync_albums": "Синхронизировать альбомы", diff --git a/i18n/sk.json b/i18n/sk.json index 197b9717c5..c8c3c69802 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -311,7 +311,7 @@ "search_jobs": "Vyhľadať úlohy…", "send_welcome_email": "Odoslať uvítací e-mail", "server_external_domain_settings": "Externá doména", - "server_external_domain_settings_description": "Verejná doména pre zdieľané odkazy, vrátane http(s)://", + "server_external_domain_settings_description": "Doména používaná pre externé odkazy", "server_public_users": "Verejní používatelia", "server_public_users_description": "Všetci používatelia (meno a email) sú uvedení pri pridávaní používateľa do zdieľaných albumov. Ak je táto funkcia vypnutá, zoznam používateľov bude dostupný iba správcom.", "server_settings": "Server", @@ -794,6 +794,11 @@ "color": "Farba", "color_theme": "Farba témy", "command": "Príkaz", + "command_palette_prompt": "Rýchlo vyhľadajte stránky, akcie alebo príkazy ako", + "command_palette_to_close": "zatvoriť", + "command_palette_to_navigate": "vložiť", + "command_palette_to_select": "vybrať", + "command_palette_to_show_all": "zobraziť všetko", "comment_deleted": "Komentár bol odstránený", "comment_options": "Možnosti komentára", "comments_and_likes": "Komentáre a páči sa mi to", @@ -997,6 +1002,11 @@ "editor_close_without_save_prompt": "Úpravy nebudú uložené", "editor_close_without_save_title": "Zavrieť editor?", "editor_confirm_reset_all_changes": "Naozaj chcete zrušiť všetky zmeny?", + "editor_discard_edits_confirm": "Zrušiť úpravy", + "editor_discard_edits_prompt": "Máte neuložené úpravy. Naozaj ich chcete zrušiť?", + "editor_discard_edits_title": "Zrušiť úpravy?", + "editor_edits_applied_error": "Nepodarilo sa použiť úpravy", + "editor_edits_applied_success": "Úpravy boli úspešne vykonané", "editor_flip_horizontal": "Prevrátiť horizontálne", "editor_flip_vertical": "Prevrátiť vertikálne", "editor_orientation": "Orientácia", @@ -1163,6 +1173,7 @@ "exif_bottom_sheet_people": "ĽUDIA", "exif_bottom_sheet_person_add_person": "Pridať meno", "exit_slideshow": "Opustiť prezentáciu", + "expand": "Rozbaliť", "expand_all": "Rozbaliť všetko", "experimental_settings_new_asset_list_subtitle": "Prebiehajúca práca", "experimental_settings_new_asset_list_title": "Povolenie experimentálnej mriežky fotografií", @@ -1608,7 +1619,6 @@ "not_available": "Nedostupné", "not_in_any_album": "Nie je v žiadnom albume", "not_selected": "Nevybrané", - "note_apply_storage_label_to_previously_uploaded assets": "Poznámka: Ak chcete použiť Štítok úložiska na predtým nahrané médiá, spustite príkaz", "notes": "Poznámky", "nothing_here_yet": "Zatiaľ tu nič nie je", "notification_permission_dialog_content": "Ak chcete povoliť upozornenia, prejdite do Nastavenia a vyberte možnosť Povoliť.", @@ -1638,6 +1648,7 @@ "online": "Online", "only_favorites": "Len obľúbené", "open": "Otvoriť", + "open_calendar": "Otvoriť kalendár", "open_in_map_view": "Otvoriť v mape", "open_in_openstreetmap": "Otvoriť v OpenStreetMap", "open_the_search_filters": "Otvoriť vyhľadávacie filtre", @@ -1810,7 +1821,7 @@ "reassigned_assets_to_new_person": "Opätovne {count, plural, one {priradená # položka} few {priradené # položky} other {priradených # položiek}} novej osobe", "reassing_hint": "Priradí zvolenú položku k existujúcej osobe", "recent": "Nedávne", - "recent-albums": "Posledné albumy", + "recent_albums": "Posledné albumy", "recent_searches": "Posledné vyhľadávania", "recently_added": "Nedávno pridané", "recently_added_page_title": "Nedávno pridané", @@ -2179,6 +2190,7 @@ "support": "Podpora", "support_and_feedback": "Podpora a spätná väzba", "support_third_party_description": "Vaša inštalácia Immich bola pripravená treťou stranou. Problémy, ktoré sa vyskytli, môžu byť spôsobené týmto balíčkom, preto sa na nich obráťte v prvom rade cez nasledujúce odkazy.", + "supporter": "Podporovateľ", "swap_merge_direction": "Vymeniť smer zlúčenia", "sync": "Synchronizovať", "sync_albums": "Synchronizovať albumy", diff --git a/i18n/sl.json b/i18n/sl.json index 76e1783f71..b4f899bb4d 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -311,7 +311,7 @@ "search_jobs": "Išči opravila…", "send_welcome_email": "Pošlji pozdravno e-pošto", "server_external_domain_settings": "Zunanja domena", - "server_external_domain_settings_description": "Domena za javne skupne povezave, vključno s http(s)://", + "server_external_domain_settings_description": "Domena, uporabljena za zunanje povezave", "server_public_users": "Javni uporabniki", "server_public_users_description": "Vsi uporabniki (ime in e-pošta) so navedeni pri dodajanju uporabnika v albume v skupni rabi. Ko je onemogočen, bo seznam uporabnikov na voljo samo skrbniškim uporabnikom.", "server_settings": "Nastavitve strežnika", @@ -782,6 +782,8 @@ "client_cert_import": "Uvozi", "client_cert_import_success_msg": "Potrdilo odjemalca je uvoženo", "client_cert_invalid_msg": "Neveljavna datoteka potrdila ali napačno geslo", + "client_cert_password_message": "Vnesite geslo za to potrdilo", + "client_cert_password_title": "Geslo potrdila", "client_cert_remove_msg": "Potrdilo odjemalca je odstranjeno", "client_cert_subtitle": "Podpira samo format PKCS12 (.p12, .pfx). Uvoz/odstranitev potrdila je na voljo samo pred prijavo", "client_cert_title": "Potrdilo odjemalca SSL [POSKUSNO]", @@ -792,6 +794,11 @@ "color": "Barva", "color_theme": "Barva teme", "command": "Ukaz", + "command_palette_prompt": "Hitro iskanje strani, dejanj ali ukazov", + "command_palette_to_close": "zapreti", + "command_palette_to_navigate": "vstopiti", + "command_palette_to_select": "izbrati", + "command_palette_to_show_all": "prikazati vse", "comment_deleted": "Komentar izbrisan", "comment_options": "Možnosti komentiranja", "comments_and_likes": "Komentarji in všečki", @@ -995,6 +1002,11 @@ "editor_close_without_save_prompt": "Spremembe ne bodo shranjene", "editor_close_without_save_title": "Zapri urejevalnik?", "editor_confirm_reset_all_changes": "Ali ste prepričani, da želite ponastaviti vse spremembe?", + "editor_discard_edits_confirm": "Zavrzi urejanja", + "editor_discard_edits_prompt": "Imate neshranjene spremembe. Ste prepričani, da jih želite zavreči?", + "editor_discard_edits_title": "Zavržem urejanja?", + "editor_edits_applied_error": "Urejanja ni bilo mogoče uporabiti", + "editor_edits_applied_success": "Spremembe so bile uspešno uporabljene", "editor_flip_horizontal": "Obrni vodoravno", "editor_flip_vertical": "Obrni navpično", "editor_orientation": "Usmerjenost", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "OSEBE", "exif_bottom_sheet_person_add_person": "Dodaj ime", "exit_slideshow": "Zapustite diaprojekcijo", + "expand": "Razširi", "expand_all": "Razširi vse", "experimental_settings_new_asset_list_subtitle": "Delo v teku", "experimental_settings_new_asset_list_title": "Omogoči eksperimentalno mrežo fotografij", @@ -1196,6 +1209,8 @@ "features_in_development": "Funkcije v razvoju", "features_setting_description": "Upravljaj funkcije aplikacije", "file_name_or_extension": "Ime ali končnica datoteke", + "file_name_text": "Ime datoteke", + "file_name_with_value": "Ime datoteke: {file_name}", "file_size": "Velikost datoteke", "filename": "Ime datoteke", "filetype": "Vrsta datoteke", @@ -1604,7 +1619,6 @@ "not_available": "Ni na voljo", "not_in_any_album": "Ni v nobenem albumu", "not_selected": "Ni izbrano", - "note_apply_storage_label_to_previously_uploaded assets": "Opomba: Če želite oznako za shranjevanje uporabiti za predhodno naložena sredstva, zaženite", "notes": "Opombe", "nothing_here_yet": "Tukaj še ni ničesar", "notification_permission_dialog_content": "Če želite omogočiti obvestila, pojdite v Nastavitve in izberite Dovoli.", @@ -1634,6 +1648,7 @@ "online": "Povezano", "only_favorites": "Samo priljubljene", "open": "Odpri", + "open_calendar": "Odpri koledar", "open_in_map_view": "Odpri v pogledu zemljevida", "open_in_openstreetmap": "Odpri v OpenStreetMap", "open_the_search_filters": "Odpri iskalne filtre", @@ -1806,7 +1821,7 @@ "reassigned_assets_to_new_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} za novo osebo", "reassing_hint": "Dodeli izbrana sredstva obstoječi osebi", "recent": "Nedavno", - "recent-albums": "Zadnji albumi", + "recent_albums": "Zadnji albumi", "recent_searches": "Nedavna iskanja", "recently_added": "Nedavno dodano", "recently_added_page_title": "Nedavno dodano", @@ -2175,6 +2190,7 @@ "support": "Podpora", "support_and_feedback": "Podpora in povratne informacije", "support_third_party_description": "Vašo namestitev Immich je pakirala tretja oseba. Težave, ki jih imate, lahko povzroči ta paket, zato prosimo, da težave najprej izpostavite njim, tako da uporabite spodnje povezave.", + "supporter": "Podpornik", "swap_merge_direction": "Zamenjaj smer združevanja", "sync": "Sinhronizacija", "sync_albums": "Sinhronizacija albumov", diff --git a/i18n/sq.json b/i18n/sq.json index 13925c212d..ba90837839 100644 --- a/i18n/sq.json +++ b/i18n/sq.json @@ -18,6 +18,7 @@ "add_a_title": "Shto një titull", "add_action": "Shto veprim", "add_action_description": "Klikoni për të shtuar një veprim për t'u kryer", + "add_assets": "Shto asete", "add_birthday": "Shto një ditëlindje", "add_endpoint": "Shto një endpoint", "add_exclusion_pattern": "Shto model përjashtimi", @@ -66,7 +67,7 @@ "backup_onboarding_title": "Kopje rezervë", "backup_settings": "Cilësimet e eksportimit të databazës", "backup_settings_description": "Menaxho cilësimet e eksportimit të databazës.", - "cleared_jobs": "Detyrat u pastruan për: {job}", + "cleared_jobs": "Punët u pastruan për: {job}", "config_set_by_file": "Konfigurimi është aktualisht vendosur nga një skedar konfigurimi", "confirm_delete_library": "A jeni i sigurt që dëshironi të fshini bibliotekën {library}?", "confirm_delete_library_assets": "A jeni i sigurt që dëshironi ta fshini këtë bibliotekë? Kjo do të fshijë {count, plural, one {# element të përmbajtur} other {të gjithë # elementët e përmbajtur}} nga Immich dhe ky veprim nuk mund të zhbëhet. Skedarët do të mbeten në disk.", @@ -75,7 +76,7 @@ "confirm_user_password_reset": "A jeni i sigurt që dëshironi të rivendosni fjalëkalimin e {user}?", "confirm_user_pin_code_reset": "A jeni i sigurt që dëshironi të rivendosni kodin PIN të {user}?", "copy_config_to_clipboard_description": "Kopjo konfigurimin aktual të sistemit si objekt JSON në clipboard", - "create_job": "Krijo detyrë", + "create_job": "Krijo punë", "cron_expression_description": "Vendosni intervalin e skanimit duke përdorur formatin Cron. Për më shumë informacion, ju lutem shikoni p.sh. Crontab Guru", "disable_login": "Çaktivizo hyrjen", "duplicate_detection_job_description": "Ekzekuto mësimin makinerik mbi skedarët për të zbuluar imazhe të ngjashme. Bazohet në Smart Search", @@ -84,7 +85,7 @@ "external_libraries_page_description": "Faqja e bibliotekës së jashtme për administratorin", "face_detection": "Zbulimi i fytyrave", "face_detection_description": "Zbulo fytyrat në skedarë duke përdorur mësimin makinerik. Për videot, konsiderohet vetëm miniatura. “Rifresko” (Refresh) përpunon përsëri të gjithë skedarët. “Rivendos” (Reset) gjithashtu fshin të gjitha të dhënat aktuale të fytyrave. “Mungon” (Missing) vendos në pritje skedarët që ende nuk janë përpunuar. Fytyrat e zbuluara do të vendosen në pritje për Njohjen e Fytyrave pas përfundimit të Zbulimit të Fytyrave, duke i grupuar ato te personat ekzistues ose të rinj.", - "failed_job_command": "Komanda {command} dështoi për detyrën: {job}", + "failed_job_command": "Komanda {command} dështoi për punën: {job}", "force_delete_user_warning": "KUJDES: Kjo do të heqë menjëherë përdoruesin dhe të gjithë skedarët e tij. Ky veprim nuk mund të zhbëhet dhe skedarët nuk mund të rikuperohen.", "image_format": "Formati", "image_format_description": "WebP prodhon skedarë më të vegjël se JPEG, por kodimi i tij është më i ngadaltë.", @@ -95,7 +96,25 @@ "image_prefer_embedded_preview": "Prefero parapamjen e integruar", "image_prefer_embedded_preview_setting_description": "Përdor parapamjet e integruara në fotot te papërpunuara si hyrje për përpunimin e imazhit, kur janë të disponueshme. Kjo mund të japë ngjyra më të sakta për disa imazhe, por cilësia e parapamjes varet nga kamera dhe imazhi mund të ketë më shumë artefakte të kompresimit.", "image_prefer_wide_gamut": "Prefero gamën e gjerë të ngjyrave", - "image_preview_title": "Cilësimet e parapamjes" + "image_preview_quality_description": "Cilësia e shikimit paraprak nga 1-100. Një cilësi më e lartë është më e mirë, por prodhon skedarë më të mëdhenj dhe mund të zvogëlojë reagimin e aplikacionit. Vendosja e një vlere të ulët mund të ndikojë në cilësinë e të mësuarit automatik.", + "image_preview_title": "Cilësimet e parapamjes", + "image_progressive_description": "Kodimi i imazheve JPEG bëhet në mënyrë progresive për një shfaqje me ngarkim gradual. Kjo nuk ka efekt në imazhet WebP.", + "image_quality": "Cilësia", + "image_resolution": "Rezolucioni", + "image_resolution_description": "Rezolucionet më të larta mund të ruajnë më shumë detaje, por kërkojnë më shumë kohë për t'u koduar, kanë madhësi më të mëdha skedarësh dhe mund të zvogëlojnë reagimin e aplikacionit.", + "image_settings": "Cilësimet e imazhit", + "image_settings_description": "Menaxhoni cilësinë dhe rezolucionin e imazheve të gjeneruara", + "image_thumbnail_quality_description": "Cilësia e miniaturave nga 1-100. Sa më e lartë të jetë aq më mirë, por prodhon skedarë më të mëdhenj dhe mund të zvogëlojë reagimin e aplikacionit.", + "image_thumbnail_title": "Cilësimet e miniaturës", + "import_config_from_json_description": "Importo konfigurimin e sistemit duke ngarkuar një skedar konfigurimi JSON", + "job_concurrency": "{job} paralele", + "job_created": "Puna u krijua", + "job_not_concurrency_safe": "Kjo punë nuk është e sigurt për paralelizmin.", + "job_settings": "Cilësimet e punës", + "job_settings_description": "Menaxhoni konkurencën e punës", + "library_scanning": "Skanimi periodik", + "library_scanning_description": "Konfiguro skanimin periodik të bibliotekës", + "library_scanning_enable_description": "Aktivizo skanimin periodik të bibliotekës" }, "download_original": "Shkarko origjinalin", "download_paused": "Shkarkimi u pezullua", diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index d656ac248e..ad4dd8ecca 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -1280,7 +1280,6 @@ "not_available": "Недоступно", "not_in_any_album": "Нема ни у једном албуму", "not_selected": "Није изабрано", - "note_apply_storage_label_to_previously_uploaded assets": "Напомена: Да бисте применили ознаку за складиштење на претходно уплоадиране датотеке, покрените", "notes": "Напомене", "notification_permission_dialog_content": "Да би укљуцили нотификације, идите у Опције и одаберите Дозволи.", "notification_permission_list_tile_content": "Дајте дозволу за омогућавање обавештења.", @@ -1448,7 +1447,7 @@ "reassigned_assets_to_new_person": "Поново додељено {count, plural, one {# датотека} other {# датотеке}} новој особи", "reassing_hint": "Доделите изабрана средства постојећој особи", "recent": "Скорашњи", - "recent-albums": "Недавни албуми", + "recent_albums": "Недавни албуми", "recent_searches": "Скорашње претраге", "recently_added": "Недавно додато", "recently_added_page_title": "Недавно Додато", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index b6f36d8c70..b59f86f37c 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -1241,7 +1241,6 @@ "no_shared_albums_message": "Napravite album da biste delili fotografije i video zapise sa ljudima u vašoj mreži", "not_in_any_album": "Nema ni u jednom albumu", "not_selected": "Nije izabrano", - "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primenili oznaku za skladištenje na prethodno uploadirane datoteke, pokrenite", "notes": "Napomene", "notification_permission_dialog_content": "Da bi ukljucili notifikacije, idite u Opcije i odaberite Dozvoli.", "notification_permission_list_tile_content": "Dajte dozvolu za omogućavanje obaveštenja.", @@ -1399,7 +1398,7 @@ "reassigned_assets_to_new_person": "Ponovo dodeljeno {count, plural, one {# datoteka} other {# datoteke}} novoj osobi", "reassing_hint": "Dodelite izabrana sredstva postojećoj osobi", "recent": "Skorašnji", - "recent-albums": "Nedavni albumi", + "recent_albums": "Nedavni albumi", "recent_searches": "Skorašnje pretrage", "recently_added": "Nedavno dodato", "recently_added_page_title": "Nedavno Dodato", diff --git a/i18n/sv.json b/i18n/sv.json index 280af17550..ddc6a1b336 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -443,7 +443,7 @@ "version_check_enabled_description": "Aktivera versionskontroll", "version_check_implications": "Funktionen för versionskontroll är beroende av periodisk kommunikation med github.com", "version_check_settings": "Versionskontroll", - "version_check_settings_description": "Aktivera/inaktivera meddelandet om ny versionen", + "version_check_settings_description": "Aktivera/inaktivera notis om ny version", "video_conversion_job": "Omkoda videor", "video_conversion_job_description": "Koda om videor för bredare kompatibilitet med webbläsare och enheter" }, @@ -782,6 +782,8 @@ "client_cert_import": "Importera", "client_cert_import_success_msg": "Klientcertifikatet är importerat", "client_cert_invalid_msg": "Felaktig certifikatfil eller fel lösenord", + "client_cert_password_message": "Ange lösenordet för detta certifikat", + "client_cert_password_title": "Certifikatlösenord", "client_cert_remove_msg": "Klientcertifikatet är borttaget", "client_cert_subtitle": "Stödjer endast formatet PKCS12 (.p12, .pfx). import/borttagning av certifikat är tillgängligt endast före inloggning", "client_cert_title": "SSL klientcertifikat [EXPERIMENTELLT]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "Ändringarna kommer inte att sparas", "editor_close_without_save_title": "Stäng redigeraren?", "editor_confirm_reset_all_changes": "Är du säker på att du vill återställa alla ändringar?", + "editor_discard_edits_confirm": "Ignorera redigeringar", + "editor_discard_edits_prompt": "Du har redigeringar som inte har sparats. Är du säker på att du vill slänga dem?", + "editor_discard_edits_title": "Släng redigeringar?", + "editor_edits_applied_error": "Misslyckades att verkställa redigeringar", + "editor_edits_applied_success": "Redigeringarna har tillämpats framgångsrikt", "editor_flip_horizontal": "Vänd horisontellt", "editor_flip_vertical": "Vänd vertikalt", "editor_orientation": "Orientering", @@ -1196,6 +1203,8 @@ "features_in_development": "Funktioner i utveckling", "features_setting_description": "Hantera appens funktioner", "file_name_or_extension": "Filnamn eller -tillägg", + "file_name_text": "Filnamn", + "file_name_with_value": "Filnamn: {file_name}", "file_size": "Filstorlek", "filename": "Filnamn", "filetype": "Filtyp", @@ -1604,7 +1613,6 @@ "not_available": "N/A", "not_in_any_album": "Inte i något album", "not_selected": "Ej vald", - "note_apply_storage_label_to_previously_uploaded assets": "Obs: Om du vill använda lagringsetiketten på tidigare uppladdade tillgångar kör du", "notes": "Notera", "nothing_here_yet": "Inget här ännu", "notification_permission_dialog_content": "För att aktivera notiser, gå till Inställningar och välj tillåt.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "Tilldelade om {count, plural, one {# objekt} other {# objekt}} till en ny persson", "reassing_hint": "Tilldela valda tillgångar till en befintlig person", "recent": "Nyligen", - "recent-albums": "Senaste album", + "recent_albums": "Senaste album", "recent_searches": "Senaste sökningar", "recently_added": "Nyligen tillagda", "recently_added_page_title": "Nyligen tillagda", diff --git a/i18n/ta.json b/i18n/ta.json index e27bdfd0cb..90a7ce6664 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -18,6 +18,7 @@ "add_a_title": "தலைப்பு சேர்க்கவும்", "add_action": "செயலைச் சேர்", "add_action_description": "செய்ய வேண்டிய செயலைச் சேர்க்க கிளிக் செய்யவும்", + "add_assets": "ஊடங்களை சேர்க்கவும்", "add_birthday": "பிறந்தநாளைச் சேர்க்கவும்", "add_endpoint": "சேவை நிரலை சேர்", "add_exclusion_pattern": "விலக்கு வடிவத்தைச் சேர்க்கவும்", @@ -187,6 +188,7 @@ "machine_learning_smart_search_enabled": "ஸ்மார்ட் தேடலை இயக்கு", "machine_learning_smart_search_enabled_description": "முடக்கப்பட்டிருந்தால், ஸ்மார்ட் தேடலுக்காக படங்கள் குறியாக்கம் செய்யப்படாது.", "machine_learning_url_description": "இயந்திர கற்றல் சேவையகத்தின் முகவரி. ஒன்றுக்கு மேற்பட்ட முகவரி வழங்கப்பட்டால், ஒவ்வொரு சேவையகமும் ஒவ்வொன்றாக வெற்றிகரமாக பதிலளிக்கும் வரை, முதலில் இருந்து கடைசி வரை முயற்சிக்கப்படும். பதிலளிக்காத சேவையகங்கள் மீண்டும் ஆன்லைனில் வரும் வரை தற்காலிகமாகப் புறக்கணிக்கப்படும்.", + "maintenance_delete_backup": "காப்புக்களை நீக்கவும்", "maintenance_settings": "பராமரிப்பு", "maintenance_settings_description": "இம்மிச்சை பராமரிப்பு முறையில் வைக்கவும்.", "maintenance_start": "பராமரிப்பு பயன்முறையைத் தொடங்கு", @@ -1480,7 +1482,6 @@ "not_available": "இதற்கில்லை", "not_in_any_album": "எந்த ஆல்பத்திலும் இல்லை", "not_selected": "தேர்ந்தெடுக்கப்படவில்லை", - "note_apply_storage_label_to_previously_uploaded assets": "குறிப்பு: முன்னர் பதிவேற்றப்பட்ட சொத்துக்களுக்கு சேமிப்பக லேபிளை பயன்படுத்த, இயக்கவும்", "notes": "குறிப்புகள்", "nothing_here_yet": "இன்னும் இங்கே எதுவும் இல்லை", "notification_permission_dialog_content": "அறிவிப்புகளை இயக்க, அமைப்புகளுக்குச் சென்று இசைவு என்பதைத் தேர்ந்தெடுக்கவும்.", @@ -1676,7 +1677,7 @@ "reassigned_assets_to_new_person": "புதிய நபருக்கு {count, plural, one {# சொத்து} other {# சொத்துகள்}} மீண்டும் ஒதுக்கப்பட்டது", "reassing_hint": "தேர்ந்தெடுக்கப்பட்ட சொத்துக்களை ஏற்கனவே இருக்கும் நபருக்கு ஒதுக்குங்கள்", "recent": "அண்மைக் கால", - "recent-albums": "அண்மைக் கால ஆல்பங்கள்", + "recent_albums": "அண்மைக் கால ஆல்பங்கள்", "recent_searches": "அண்மைக் கால தேடல்கள்", "recently_added": "அண்மைக் காலத்தில் சேர்க்கப்பட்டது", "recently_added_page_title": "அண்மைக் காலத்தில் சேர்க்கப்பட்டது", diff --git a/i18n/te.json b/i18n/te.json index d9d24bb3c6..275b2deb42 100644 --- a/i18n/te.json +++ b/i18n/te.json @@ -895,7 +895,6 @@ "no_results_description": "పర్యాయపదం లేదా మరింత సాధారణ కీవర్డ్‌ని ప్రయత్నించండి", "no_shared_albums_message": "మీ నెట్‌వర్క్‌లోని వ్యక్తులతో ఫోటోలు మరియు వీడియోలను భాగస్వామ్యం చేయడానికి ఆల్బమ్‌ను సృష్టించండి", "not_in_any_album": "ఏ ఆల్బమ్‌లోనూ లేదు", - "note_apply_storage_label_to_previously_uploaded assets": "గమనిక: గతంలో అప్‌లోడ్ చేసిన ఆస్తులకు నిల్వ లేబుల్‌ను వర్తింపజేయడానికి,", "notes": "గమనికలు", "notification_toggle_setting_description": "ఇమెయిల్ నోటిఫికేషన్‌లను ప్రారంభించండి", "notifications": "నోటిఫికేషన్‌లు", @@ -1021,7 +1020,7 @@ "reassign": "తిరిగి కేటాయించు", "reassing_hint": "ఎంచుకున్న ఆస్తులను ఇప్పటికే ఉన్న వ్యక్తికి కేటాయించండి", "recent": "ఇటీవలి", - "recent-albums": "ఇటీవలి ఆల్బమ్‌లు", + "recent_albums": "ఇటీవలి ఆల్బమ్‌లు", "recent_searches": "ఇటీవలి శోధనలు", "refresh": "రిఫ్రెష్ చేయి", "refresh_encoded_videos": "ఎన్‌కోడ్ చేసిన వీడియోలను రిఫ్రెష్ చేయండి", diff --git a/i18n/th.json b/i18n/th.json index abe9b93f19..ee53ff2c9f 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -6,8 +6,8 @@ "action": "ดำเนินการ", "action_common_update": "อัปเดต", "actions": "การดำเนินการ", - "active": "ใช้งานอยู่", - "active_count": "ใช้งานอยู่: {count}", + "active": "กำลังทำงาน", + "active_count": "กำลังทำงาน: {count}", "activity": "กิจกรรม", "activity_changed": "กิจกรรม{enabled, select, true {เปิด} other {ปิด}}อยู่", "add": "เพิ่ม", @@ -16,6 +16,7 @@ "add_a_name": "เพิ่มชื่อ", "add_a_title": "เพิ่มหัวข้อ", "add_action": "เพิ่มการดำเนินการ", + "add_assets": "เพิ่มสื่อ", "add_birthday": "เพิ่มวันเกิด", "add_endpoint": "เพิ่มปลายทาง", "add_exclusion_pattern": "เพิ่มข้อยกเว้น", @@ -26,7 +27,7 @@ "add_path": "เพิ่มพาทที่ตั้ง", "add_photos": "เพิ่มรูปภาพ", "add_tag": "เพิ่มแท็ก", - "add_to": "เพิ่มไปยัง …", + "add_to": "เพิ่มไปยัง…", "add_to_album": "เพิ่มไปยังอัลบั้ม", "add_to_album_bottom_sheet_added": "เพิ่มไปยัง {album} แล้ว", "add_to_album_bottom_sheet_already_exists": "อยู่ใน {album} อยู่แล้ว", @@ -53,7 +54,7 @@ "backup_database_enable_description": "เปิดใช้งานสำรองฐานข้อมูล", "backup_keep_last_amount": "จำนวนข้อมูลสำรองก่อนหน้าที่ต้องเก็บไว้", "backup_onboarding_1_description": "สำเนานอกสถานที่บนคลาวด์หรือที่ตั้งอื่น", - "backup_onboarding_2_description": "สำเนาที่อยู่บนเครื่องต่างกัน ซึ่งรวมถึงไฟล์หลักและไฟล์สำรองบนเครื่อง", + "backup_onboarding_2_description": "สำเนาที่อยู่บนอุปกรณ์ที่ต่างกัน ซึ่งรวมถึงไฟล์หลักและไฟล์สำรองบนเครื่อง", "backup_onboarding_3_description": "จำนวนชุดของข้อมูลทั้งหมด รวมถึงไฟล์เดิม ซึ่งรวมถึง 1 ชุดที่ตั้งอยู่คนละถิ่น และสำเนาบนเครื่อง 2 ชุด", "backup_onboarding_description": "แนะนำให้ใช้ การสำรองข้อมูลแบบ 3-2-1เพื่อปกป้องข้อมูล ควรเก็บสำเนาของรูปภาพ/วิดีโอที่อัปโหลดและฐานข้อมูลของ Immich เพื่อสำรองข้อมูลได้อย่างทั่วถึง", "backup_onboarding_footer": "สำหรับข้อมูลเพิ่มเติมที่เกี่ยวกับการสำรองข้อมูลของ Immich โปรดดูที่ documentation", @@ -64,7 +65,7 @@ "cleared_jobs": "เคลียร์งานสำหรับ: {job}", "config_set_by_file": "การตั้งค่าคอนฟิกกำลังถูกกำหนดโดยไฟล์คอนฟิก", "confirm_delete_library": "คุณแน่ใจว่าอยากลบคลังภาพ {library} หรือไม่?", - "confirm_delete_library_assets": "คุณแน่ใจว่าอยากลบคลังภาพนี้หรือไม่? สี่อทั้งหมด {count, plural, one {# สื่อ} other {all # สื่อ}} สี่อในคลังจะถูกลบออกจาก Immich โดยถาวร ไฟล์จะยังคงอยู่บนดิสก์", + "confirm_delete_library_assets": "คุณแน่ใจว่าต้องการลบคลังภาพนี้หรือไม่? สี่อทั้งหมด {count, plural, one {# สื่อ} other {all # สื่อ}} สี่อในคลังจะถูกลบออกจาก Immich โดยถาวร ไฟล์จะยังคงอยู่บนดิสก์", "confirm_email_below": "โปรดยืนยัน โดยการพิมพ์ \"{email}\" ข้างล่าง", "confirm_reprocess_all_faces": "คุณแน่ใจว่าคุณต้องการประมวลผลใบหน้าทั้งหมดใหม่? ชื่อคนจะถูกลบไปด้วย", "confirm_user_password_reset": "คุณแน่ใจว่าต้องการรีเซ็ตรหัสผ่านของ {user} หรือไม่?", @@ -131,6 +132,10 @@ "logging_level_description": "เมื่อเปิดใช้งาน ใช้ระดับการบันทึกอะไร", "logging_settings": "การบันทึก", "machine_learning_availability_checks_description": "ตรวจจับและเลือกใช้เซิร์ฟเวอร์ machine learning โดยอัตโนมัติ", + "machine_learning_availability_checks_interval": "ระยะเวลาตรวจสอบ", + "machine_learning_availability_checks_interval_description": "ระยะเวลาเป็นมิลลิวินาทีระหว่างการตรวจสอบความพร้อมแต่ละครั้ง", + "machine_learning_availability_checks_timeout": "คำขอหมดเวลา", + "machine_learning_availability_checks_timeout_description": "จำนวนมิลลิวินาทีที่จะนับว่าหมดเวลาสำหรับการตรวจสอบความพร้อม", "machine_learning_clip_model": "โมเดล Clip", "machine_learning_clip_model_description": "ชื่อของโมเดล CLIP ที่ระบุตรงนี้ โปรดทราบว่าจำเป็นต้องดำเนินงาน 'ค้นหาอัจฉริยะ' ใหม่สำหรับทุกรูปเมื่อเปลี่ยนโมเดล", "machine_learning_duplicate_detection": "ตรวจจับการซ้ำกัน", @@ -169,6 +174,8 @@ "machine_learning_smart_search_enabled": "เปิดใช้งานการค้นหาอัจฉริยะ", "machine_learning_smart_search_enabled_description": "หากปิดใช้งาน ภาพจะไม่ถูกใช้สําหรับการค้นหาอัจฉริยะ", "machine_learning_url_description": "URL ของเซิร์ฟเวอร์ machine learning กรณีมี URL มากกว่าหนึ่ง URL จะทำการทดลองส่งข้อมูลเรียงไปทีละอันตามลำดับจนกว่าจะพบ URL ที่ตอบสนอง และจะเลิกส่งข้อมูลชั่วคราวในส่วนของ URL ที่ไม่ตอบสนอง", + "maintenance_delete_backup": "ลบการสำรองข้อมูล", + "maintenance_delete_backup_description": "ไฟล์นี้จะถูกลบและไม่สามารถย้อนกลับได้", "manage_concurrency": "จัดการการทำงานพร้อมกัน", "manage_log_settings": "จัดการการตั้งค่าจดบันทึก", "map_dark_style": "แบบมืด", @@ -193,9 +200,14 @@ "metadata_settings": "การตั้งค่า Metadata", "metadata_settings_description": "จัดการการตั้งค่า Metadata", "migration_job": "การโยกย้าย", - "migration_job_description": "ย้ายภาพตัวอย่างสื่อและใบหน้าไปยังโครงสร้างโฟลเดอร์ล่าสุด", + "migration_job_description": "ย้ายภาพตัวอย่างสำหรับสื่อและใบหน้าไปยังโครงสร้างโฟลเดอร์ล่าสุด", "nightly_tasks_cluster_new_faces_setting": "คลัสเตอร์ใบหน้าใหม่", "nightly_tasks_generate_memories_setting": "สร้างความทรงจำ", + "nightly_tasks_generate_memories_setting_description": "สร้างความทรงจำใหม่จากสื่อ", + "nightly_tasks_missing_thumbnails_setting": "สร้างภาพขนาดย่อที่ขาดหายไป", + "nightly_tasks_missing_thumbnails_setting_description": "เพิ่มสื่อที่ไม่มีภาพขนาดย่อไปยังคิวเพื่อสร้างภาพขนาดย่อ", + "nightly_tasks_start_time_setting": "เวลาเริ่มต้น", + "nightly_tasks_start_time_setting_description": "เวลาที่เซิร์ฟเวอร์จะเริ่มงานประจำคืน", "no_paths_added": "ไม่ได้เพิ่มพาธ", "no_pattern_added": "ไม่ได้เพิ่มรูปแบบ", "note_apply_storage_label_previous_assets": "หากต้องการใช้ Storage Label กับไฟล์ที่อัปโหลดก่อนหน้านี้ ให้รันคำสั่งนี้", @@ -327,7 +339,7 @@ "transcoding_max_b_frames": "B-frames สูงสุด", "transcoding_max_b_frames_description": "ค่าที่สูงขึ้นจะช่วยเพิ่มประสิทธิภาพในการบีบอัด แต่จะทำให้การเข้ารหัสช้าลง อาจไม่สามารถใช้งานร่วมกับการเร่งความเร็วฮาร์ดแวร์บนอุปกรณ์เก่าได้ ค่าที่เป็น 0 จะปิดการใช้งาน B-frame ในขณะที่ค่า -1 จะตั้งค่าค่านี้โดยอัตโนมัติ", "transcoding_max_bitrate": "bitrate สูงสุด", - "transcoding_max_bitrate_description": "การตั้งค่า bitrate สูงสุดจะสามารถคาดเดาขนาดไฟล์ได้มากขึ้นโดยไม่กระทบคุณภาพ สำหรับความคมชัด 720p ค่าทั่วไปคือ 2600 kbit/s สําหรับ VP9 หรือ HEVC, 4500 kbit/s สําหรับ H.264 ปิดการตั้งค่าเมี่อตั้งค่าเป็น 0", + "transcoding_max_bitrate_description": "การตั้งค่า bitrate สูงสุดจะสามารถคาดเดาขนาดไฟล์ได้ง่ายขึ้นโดยกระทบคุณภาพเล็กน้อย ค่าทั่วไปสำหรับความคมชัด 720p คือ 2600 kbit/s สําหรับ VP9 หรือ HEVC, หรือ 4500 kbit/s สําหรับ H.264 ปิดการตั้งค่าเมี่อตั้งค่าเป็น 0 หากไม่ระบุหน่วยระบบจะถือว่าใช้หน่วย k (kbit/s) ดังนั้นค่า 5000, 5000k และ 5M (Mbit/s) ถือว่าเทียบเท่ากัน", "transcoding_max_keyframe_interval": "ช่วงเวลาสูงสุดระหว่างกราฟฟ์เคลื่อนไหว", "transcoding_max_keyframe_interval_description": "ตั้งค่าระยะห่างสูงสุดระหว่างคีย์เฟรม (keyframes) ค่าที่ต่ำลงจะทำให้ประสิทธิภาพการบีบอัดแย่ลง แต่จะช่วยปรับปรุงเวลาในการค้นหาภาพ (seek times) และอาจช่วยปรับปรุงคุณภาพในฉากที่มีการเคลื่อนไหวเร็ว ค่า 0 จะตั้งค่านี้โดยอัตโนมัติ", "transcoding_optimal_description": "วีดิโอมีความคมชัดสูงกว่าเป้าหมายหรืออยู่ในรูปแบบที่รับไม่ได้", @@ -395,7 +407,7 @@ "advanced_settings_proxy_headers_title": "พ็อกซี่ เฮดเดอร์", "advanced_settings_self_signed_ssl_subtitle": "ข้ามการตรวจสอบใบรับรอง SSL จำเป็นสำหรับใบรับรองแบบ self-signed", "advanced_settings_self_signed_ssl_title": "อนุญาตใบรับรอง SSL แบบ self-signed", - "advanced_settings_sync_remote_deletions_subtitle": "บหรือกู้คืนไฟล์บนอุปกรณ์นี้โดยอัตโนมัติเมื่อดำเนินการดังกล่าวผ่านเว็บ", + "advanced_settings_sync_remote_deletions_subtitle": "ลบหรือกู้คืนไฟล์บนอุปกรณ์นี้โดยอัตโนมัติเมื่อดำเนินการดังกล่าวผ่านเว็บ", "advanced_settings_sync_remote_deletions_title": "ซิงก์การลบจากระยะไกล [คุณสมบัติทดลอง]", "advanced_settings_tile_subtitle": "ตั้งค่าผู้ใช้งานขั้นสูง", "advanced_settings_troubleshooting_subtitle": "เปิดฟีเจอร์เพิ่มเติมเพื่อแก้ไขปัญหา", @@ -403,11 +415,13 @@ "age_months": "อายุ {months, plural, one {# เดือน} other {# เดือน}}", "age_year_months": "อายุ 1 ปี {months, plural, one {# เดือน} other {# เดือน}}", "age_years": "{years, plural, other {อายุ #}}", + "album": "อัลบั้ม", "album_added": "เพิ่มอัลบั้มแล้ว", "album_added_notification_setting_description": "แจ้งเตือนอีเมลเมื่อคุณถูกเพิ่มไปในอัลบั้มที่แชร์กัน", "album_cover_updated": "อัพเดทหน้าปกอัลบั้มแล้ว", "album_delete_confirmation": "คุณแน่ใจที่จะลบอัลบั้ม {album} นี้ ?", "album_delete_confirmation_description": "หากแชร์อัลบั้มนี้ ผู้ใช้รายอื่นจะไม่สามารถเข้าถึงได้อีก", + "album_deleted": "ลบอัลบั้มแล้ว", "album_info_card_backup_album_excluded": "ถูกยกเว้น", "album_info_card_backup_album_included": "รวม", "album_info_updated": "อัปเดทข้อมูลอัลบั้มแล้ว", @@ -417,9 +431,13 @@ "album_options": "ตัวเลือกอัลบั้ม", "album_remove_user": "ลบผู้ใช้ ?", "album_remove_user_confirmation": "คุณต้องการที่จะลบผู้ใช้ {user} ?", + "album_search_not_found": "ไม่พบอัลบั้มที่ตรงตามการค้นหาของคุณ", + "album_selected": "เลือกอัลบั้มแล้ว", "album_share_no_users": "ดูเหมือนว่าคุณได้แชร์อัลบั้มนี้กับผู้ใช้ทั้งหมดแล้ว", + "album_summary": "สรุปอัลบั้ม", "album_updated": "อัปเดทอัลบั้มแล้ว", "album_updated_setting_description": "แจ้งเตือนอีเมลเมื่ออัลบั้มที่แชร์กันมีสื่อใหม่", + "album_upload_assets": "อัปโหลดสื่อจากคอมพิวเตอร์เพื่อเพิ่มไปยังอัลบั้ม", "album_user_left": "ออกจาก {album}", "album_user_removed": "ลบผู้ใช้ {user} แล้ว", "album_viewer_appbar_delete_confirm": "คุณแน่ใจว่าอยากลบอัลบั้มนี้จากบัญชีคุณหรือไม่", @@ -436,15 +454,20 @@ "albums_default_sort_order": "การจัดเรียงอัลบั้มเริ่มต้น", "albums_default_sort_order_description": "การจัดเรียงแอสเซ็ตเริ่มต้นเมื่อสร้างอัลบั้มใหม่", "albums_feature_description": "กลุ่มของแอสเซ็ตที่สามารถส่งให้ผู้ใช้อื่นได้", + "albums_on_device_count": "อัลบั้มบนอุปกรณ์ ({count})", + "albums_selected": "{count, plural, one {เลือก # อัลบั้ม} other {เลือก # อัลบั้ม}}", "all": "ทั้งหมด", "all_albums": "อัลบั้มทั้งหมด", "all_people": "ทุกคน", + "all_photos": "รูปภาพทั้งหมด", "all_videos": "วิดีโอทั้งหมด", "allow_dark_mode": "อนุญาตโหมดมืด", "allow_edits": "อนุญาตให้แก้ไขได้", "allow_public_user_to_download": "อนุญาตให้ผู้ใช้สาธารณะดาวน์โหลดได้", "allow_public_user_to_upload": "อนุญาตให้ผู้ใช้สาธารณะอัปโหลดได้", + "allowed": "อนุญาต", "alt_text_qr_code": "รูปภาพ QR code", + "always_keep": "เก็บเสมอ", "always_keep_photos_hint": "\"เพิ่มพื้นที่ว่าง\" จะเก็บรูปภาพทั้งหมดบนอุปกรณ์นี้", "always_keep_videos_hint": "\"เพิ่มพื้นที่ว่าง\" จะเก็บวิดีโอทั้งหมดบนอุปกรณ์นี้", "anti_clockwise": "ทวนเข็มนาฬิกา", @@ -455,9 +478,13 @@ "app_bar_signout_dialog_content": "คุณแน่ใจว่าอยากออกจากระบบ", "app_bar_signout_dialog_ok": "ใช่", "app_bar_signout_dialog_title": "ออกจากระบบ", + "app_download_links": "ลิงก์ดาวน์โหลดแอป", "app_settings": "การตั้งค่าแอป", + "app_stores": "ร้านค้าแอป", + "app_update_available": "มีอัปเดตแอป", "appears_in": "อยู่ใน", "archive": "เก็บถาวร", + "archive_action_prompt": "เพิ่ม {count} รายการไปยังเก็บถาวรแล้ว", "archive_or_unarchive_photo": "เก็บ/ไม่เก็บภาพถาวร", "archive_page_no_archived_assets": "ไม่พบทรัพยากรในที่เก็บถาวร", "archive_page_title": "เก็บถาวร ({count})", @@ -471,6 +498,7 @@ "asset_action_share_err_offline": "ไม่สามารถดึงข้อมูลทรัพยากรออฟไลน์ กำลังข้าม", "asset_added_to_album": "เพิ่มไปยังอัลบั้มแล้ว", "asset_adding_to_album": "กำลังเพิ่มไปยังอัลบั้ม…", + "asset_created": "สร้างสื่อแล้ว", "asset_description_updated": "อัปเดตรายละเอียดสำเร็จ", "asset_filename_is_offline": "สื่อ {filename} ออฟไลน์อยู่", "asset_has_unassigned_faces": "สื่อไม่ได้ระบุใบหน้า", @@ -483,28 +511,35 @@ "asset_list_layout_sub_title": "การจัดวาง", "asset_list_settings_subtitle": "ตั้งค่าการจัดวางตารางรูปภาพ", "asset_list_settings_title": "ตารางรูปภาพ", + "asset_not_found_on_device_android": "ไม่พบสื่อบนอุปกรณ์", + "asset_not_found_on_device_ios": "ไม่พบสื่อบนอุปกรณ์ หากคุณใช้ iCloud สื่ออาจจะเข้าถึงไม่ได้เนื่องจาก iCloud เก็บไฟล์ที่ไม่ดีไว้", + "asset_not_found_on_icloud": "ไม่พบสื่อบน iCloud สื่ออาจจะเข้าถึงไม่ได้เนื่องจาก iCloud เก็บไฟล์ที่ไม่ดีไว้", "asset_offline": "สื่อออฟไลน์", "asset_offline_description": "ไม่พบทรัพยากรภายนอกนี้ในดิสก์อีกต่อไป โปรดติดต่อผู้ดูแลระบบ Immich ของคุณเพื่อขอความช่วยเหลือ", "asset_restored_successfully": "กู้คืนสื่อสำเร็จ", "asset_skipped": "ข้ามแล้ว", "asset_skipped_in_trash": "ในถังขยะ", + "asset_trashed": "ย้ายสื่อไปยังถังขยะแล้ว", + "asset_troubleshoot": "แก้ปัญหาสื่อ", "asset_uploaded": "อัปโหลดแล้ว", "asset_uploading": "กำลังอัปโหลด…", "asset_viewer_settings_subtitle": "ตั้งค่าการแสดงแกลเลอรี", "asset_viewer_settings_title": "ตัวดูทรัพยากร", "assets": "สื่อ", - "assets_added_count": "เพิ่ม {count, plural, one{# สื่อ} other {# สื่อ}} แล้ว", + "assets_added_count": "เพิ่มสื่อ {count, plural, one{# รายการ} other {# รายการ}}แล้ว", "assets_added_to_album_count": "เพิ่ม {count, plural, one {# asset} other {# assets}} ไปยังอัลบั้ม", + "assets_added_to_albums_count": "เพิ่มสื่อ {assetTotal, plural, one {# รายการ} other {# รายการ}} ไปยังอัลบั้ม {albumTotal, plural, one {# รายการ} other {# รายการ}}แล้ว", "assets_cannot_be_added_to_album_count": "ไม่สามารถเพิ่ม {count, plural, one {สื่อ} other {สื่อ}} ไปยังอัลบั้ม", - "assets_count": "{count, plural, one { สื่อ} other { สื่อ}}", + "assets_cannot_be_added_to_albums": "ไม่สามารถเพิ่ม{count, plural, one {สื่อ} other {สื่อ}}ไปยังอัลบั้มใด ๆ ได้", + "assets_count": "สื่อ {count, plural, one {# รายการ} other {# รายการ}}", "assets_deleted_permanently": "{count} สื่อถูกลบอย่างถาวร", - "assets_deleted_permanently_from_server": "ลบ {count} สื่อออกจาก Immich อย่างถาวร", + "assets_deleted_permanently_from_server": "ลบสื่อ {count} รายการออกจากเซิร์ฟเวอร์ Immich อย่างถาวรแล้ว", "assets_downloaded_failed": "ดาวน์โหลด {count, plural, one {ไฟล์} other {ไฟล์}} ไม่สำเร็จ - {error}", "assets_downloaded_successfully": "ดาวน์โหลด {count, plural, one {ไฟล์} other {ไฟล์}} สำเร็จ", "assets_moved_to_trash_count": "ย้าย {count, plural, one {# asset} other {# assets}} ไปยังถังขยะแล้ว", "assets_permanently_deleted_count": "ลบ {count, plural, one {# asset} other {# assets}} ทิ้งถาวร", "assets_removed_count": "{count, plural, one {# asset} other {# assets}} ถูกลบแล้ว", - "assets_removed_permanently_from_device": "นำ {count} สื่อออกจากอุปกรณ์อย่างถาวร", + "assets_removed_permanently_from_device": "ลบสื่อ {count} รายการออกจากอุปกรณ์ของคุณอย่างถาวรแล้ว", "assets_restore_confirmation": "คุณแน่ใจหรือไม่ว่าต้องการกู้คืนสื่อที่ทิ้งทั้งหมด? คุณไม่สามารถย้อนกลับการดำเนินการนี้ได้! โปรดทราบว่าสื่อออฟไลน์ใดๆ ไม่สามารถกู้คืนได้ด้วยวิธีนี้", "assets_restored_count": "{count, plural, one {# asset} other {# assets}} คืนค่า", "assets_restored_successfully": "กู้คืน {count} สื่อสำเร็จ", @@ -512,14 +547,17 @@ "assets_trashed_count": "{count, plural, one {# asset} other {# assets}} ถูกลบ", "assets_trashed_from_server": "ย้าย {count} สื่อจาก Immich ไปยังถังขยะ", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} อยู่ในอัลบั้มอยู่แล้ว", + "assets_were_part_of_albums_count": "{count, plural, one {สื่อ} other {สื่อ}}เป็นส่วนหนึ่งของอัลบั้มอยู่แล้ว", "authorized_devices": "อุปกรณ์ที่ได้รับอนุญาต", "automatic_endpoint_switching_subtitle": "เชื่อมต่อด้วย LAN ภายในวง Wi-Fi ที่ระบุไว้ และเชื่อมต่อด้วยวิธีอื่นเมื่ออยู่นอก Wi-Fi ที่ระบุไว้", "automatic_endpoint_switching_title": "สลับ URL อัตโนมัติ", "autoplay_slideshow": "เล่นสไลด์โชว์", "back": "กลับ", "back_close_deselect": "ย้อนกลับ, ปิด, หรือยกเลิกการเลือก", + "background_backup_running_error": "การสำรองข้อมูลเบื้องหลังทำงานอยู่ ไม่สามารถเริ่มสำรองข้อมูลด้วยตนเองได้", "background_location_permission": "การอนุญาตระบุตำแหน่งพื้นหลัง", "background_location_permission_content": "เพื่อที่จะสลับการเชื่อมต่อขณะที่รันในพื้นหลัง Immich ต้องรู้ตำแหน่งที่แม่ยำตลอดเวลา เพื่อจะสามารถอ่านชื่อ Wi-Fi", + "background_options": "ตัวเลือกการทำงานเบื้องหลัง", "backup": "สำรองข้อมูล", "backup_album_selection_page_albums_device": "อัลบั้มบนเครื่อง ({count})", "backup_album_selection_page_albums_tap": "กดเพื่อรวม กดสองครั้งเพื่อยกเว้น", @@ -581,8 +619,11 @@ "backup_manual_in_progress": "อัปโหลดกำลังดำเนินการอยู่ โปรดลองใหม่ในสักพัก", "backup_manual_success": "สำเร็จ", "backup_manual_title": "สถานะอัพโหลด", + "backup_options": "ตัวเลือกการสำรองข้อมูล", "backup_options_page_title": "ตัวเลือกการสำรองข้อมูล", "backup_setting_subtitle": "ตั้งค่าการอัพโหลดในฉากหน้า และพื้นหลัง", + "backup_settings_subtitle": "จัดการการตั้งค่าอัปโหลด", + "backup_upload_details_page_more_details": "แตะเพื่อดูข้อมูลเพิ่มเติม", "backward": "กลับหลัง", "biometric_auth_enabled": "การพิสูจน์อัตลักษณ์เพื่อยืนยันตัวบุคคลถูกเปิด", "biometric_locked_out": "การพิสูจน์อัตลักษณ์เพื่อยืนยันตัวบุคคลถูกล็อค", @@ -618,6 +659,7 @@ "cancel": "ยกเลิก", "cancel_search": "ยกเลิกการค้นหา", "canceled": "ยกเลิก", + "canceling": "กำลังยกเลิก", "cannot_merge_people": "ไม่สามารถรวมกลุ่มคนได้", "cannot_undo_this_action": "การกระทำนี้ไม่สามารถย้อนกลับได้!", "cannot_update_the_description": "ไม่สามารถอัพเดทรายละเอียดได้", @@ -634,18 +676,31 @@ "change_password_description": "การเข้าสู่ระบบครั้งแรก จำเป็นจต้องเปลี่ยนรหัสผ่านของคุณเพื่อความปลอดภัย โปรดป้อนรหัสผ่านใหม่ด้านล่าง", "change_password_form_confirm_password": "ยืนยันรหัสผ่าน", "change_password_form_description": "สวัสดี {name},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง", + "change_password_form_log_out": "ออกจากระบบอุปกรณ์อื่นทั้งหมด", + "change_password_form_log_out_description": "แนะนำให้ออกจากระบบอุปกรณ์อื่นทั้งหมดด้วย", "change_password_form_new_password": "รหัสผ่านใหม่", "change_password_form_password_mismatch": "รหัสผ่านไม่ตรงกัน", "change_password_form_reenter_new_password": "กรอกรหัสผ่านใหม่", "change_pin_code": "เปลี่ยนรหัสประจำตัว (PIN)", "change_your_password": "เปลี่ยนรหัสผ่านของคุณ", "changed_visibility_successfully": "เปลี่ยนการมองเห็นเรียบร้อยแล้ว", + "charging": "กำลังชาร์จ", + "charging_requirement_mobile_backup": "การสำรองข้อมูลในเบื้องหลังจะทำงานเฉพาะเมื่ออุปกรณ์กำลังชาร์จอยู่", "check_corrupt_asset_backup": "ตรวจสอบสำรองสื่อที่ผิดปกติ", "check_corrupt_asset_backup_button": "ตรวจสอบ", "check_corrupt_asset_backup_description": "ตรวจสอบเมื่อเชื่อมต่อ Wi-Fi และสื่อทั้งหมดถูกสำรองข้อมูลแล้วเท่านั้น การตรวจสอบอาจใช้เวลาหลายนาที", "check_logs": "ตรวจสอบบันทึก", "choose_matching_people_to_merge": "เลือกคนที่ตรงกันเพื่อรวมเข้าด้วยกัน", "city": "เมือง", + "cleanup_confirm_description": "Immich พบสื่อ {count} รายการ (สร้างขึ้นก่อน {date}) ที่ถูกสำรองข้อมูลบนเซิร์ฟเวอร์อย่างปลอดภัยแล้ว ลบสำเนาต้นทางจากอุปกรณ์นี้หรือไม่?", + "cleanup_confirm_prompt_title": "ลบออกจากอุปกรณ์นี้หรือไม่?", + "cleanup_deleted_assets": "ย้ายสื่อ {count} รายการไปยังถังขยะของอุปกรณ์แล้ว", + "cleanup_deleting": "กำลังย้ายไปถังขยะ...", + "cleanup_found_assets": "พบสื่อ {count} รายการที่สำรองข้อมูลแล้ว", + "cleanup_found_assets_with_size": "พบสื่อ {count} รายการที่สำรองข้อมูลแล้ว ({size})", + "cleanup_icloud_shared_albums_excluded": "อัลบั้มที่แชร์บน iCloud ไม่นับรวมในการค้นหา", + "cleanup_no_assets_found": "ไม่พบสื่อที่ตรงตามเงื่อนไขด้านบน \"เพิ่มพื้นที่ว่าง\" สามารถลบได้เฉพาะสื่อที่สำรองข้อมูลบนเซิร์ฟเวอร์เรียบร้อยแล้วเท่านั้น", + "cleanup_preview_title": "สื่อที่จะลบ ({count})", "clear": "ล้าง", "clear_all": "ล้างทั้งหมด", "clear_all_recent_searches": "ล้างประวัติการค้นหา", @@ -788,8 +843,9 @@ "display_original_photos_setting_description": "การตั้งค่าแสดงผลรูปภาพต้นฉบับ เมื่อเปิดรูปภาพ การตั้งค่านี้อาจจะทำให้การแสดงภาพได้ช้าลง", "do_not_show_again": "ไม่แสดงข้อความนี้อีก", "documentation": "เอกสาร", - "done": "ดำเนินการสำเร็จ", + "done": "เสร็จสิ้น", "download": "ดาวน์โหลด", + "download_action_prompt": "กำลังดาวน์โหลด {count} ชิ้น", "download_canceled": "การดาวน์โหลดยกเลิก", "download_complete": "การดาวน์โหลดเสร็จสิ้น", "download_enqueue": "การดาวน์โหลดอยู่ในคิว", @@ -799,6 +855,7 @@ "download_include_embedded_motion_videos": "รวมวิดีโอที่ฝังอยู่ในภาพเคลื่อนไหว", "download_include_embedded_motion_videos_description": "รวมวิดีโอที่ฝังอยู่ในภาพเคลื่อนไหวเมื่อดาวน์โหลดอัลบั้ม", "download_notfound": "ไม่พบดาวน์โหลด", + "download_original": "ดาวน์โหลดตัวตั้งต้น", "download_paused": "หยุดการดาวน์โหลดชั่วคราว", "download_settings": "การตั้งค่าการดาวน์โหลด", "download_settings_description": "จัดการการตั้งค่าการดาวน์โหลด", @@ -808,6 +865,7 @@ "download_waiting_to_retry": "รอลองใหม่", "downloading": "กำลังดาวน์โหลด", "downloading_asset_filename": "กำลังดาวน์โหลด {filename}", + "downloading_from_icloud": "กำลังดาวน์โหลดจากไอคลาว", "downloading_media": "กำลังดาวน์โหลดสื่อ", "drop_files_to_upload": "วางไฟล์ในช่องอัปโหลด", "duplicates": "รายการที่ซ้ำกัน", @@ -816,6 +874,7 @@ "edit": "แก้ไข", "edit_album": "แก้ไขอัลบั้ม", "edit_avatar": "แก้ไขตัวละคร", + "edit_birthday": "แก้ไขวันเกิด", "edit_date": "แก้ไขวันที่", "edit_date_and_time": "แก้ไขวันที่และเวลา", "edit_description": "แก้ไขคำอธิบาย", @@ -825,15 +884,29 @@ "edit_key": "แก้ไขกุญแจ", "edit_link": "แก้ไขลิงก์", "edit_location": "แก้ไขตำแหน่ง", + "edit_location_action_prompt": "{count} จุดที่ถูกแก้ไข", "edit_location_dialog_title": "ตำแหน่ง", "edit_name": "แก้ไขชื่อ", "edit_people": "แก้ไขผู้คน", "edit_tag": "แก้ไขแท็ก", "edit_title": "แก้ไขชื่อ", "edit_user": "แก้ไขผู้ใช้", + "edit_workflow": "แก้ไขกระบวนการงาน", "editor": "ผู้แก้ไข", "editor_close_without_save_prompt": "การเปลี่ยนแปลงนี้จะไม่ได้รับการบันทึก", "editor_close_without_save_title": "ปิดโปรแกรมแก้ไข?", + "editor_confirm_reset_all_changes": "แน่ใจว่าจะยกเลิกการแก้ไขทั้งหมด", + "editor_discard_edits_confirm": "ยกเลิกการแก้ไข", + "editor_discard_edits_prompt": "คุณมีการแก้ไขที่ยังไม่ได้บันทึก แน่ใจว่าจะยกเลิกการแก้ไขทั้งหมด", + "editor_discard_edits_title": "ยกเลิกการแก้ไข", + "editor_edits_applied_error": "การแก้ไขล้มเหลว", + "editor_edits_applied_success": "การแก้ไขถูกบันทึกสำเร็จแล้ว", + "editor_flip_horizontal": "กลับด้านทางแนวนอน", + "editor_flip_vertical": "กลับด้านทางแนวตั้ง", + "editor_orientation": "การวางแนว", + "editor_reset_all_changes": "ยกเลิกการแก้ไข", + "editor_rotate_left": "หมุน 90 องศาทวนเข็มนาฬิกา", + "editor_rotate_right": "หมุน 90 องศาตามเข็มนาฬิกา", "email": "อีเมล", "email_notifications": "แจ้งเตือนผ่านอีเมล", "empty_folder": "โฟลเดอร์นี้ว่างเปล่า", @@ -991,7 +1064,7 @@ "export_as_json": "ส่งออกเป็นไฟล์ JSON", "extension": "ส่วนต่อขยาย", "external": "ภายนอก", - "external_libraries": "ภายนอกคลังภาพ", + "external_libraries": "คลังภาพภายนอก", "external_network": "การเชื่อมต่อภายนอก", "external_network_sheet_info": "เมื่อไม่ได้เชื่อมต่อ Wi-Fi ที่เลือกไว้ แอพจะเชื่อมต่อเซิร์ฟเวอร์ผ่าน URL ด้านล่างตามลำดับ", "face_unassigned": "ไม่กำหนดมอบหมาย", @@ -1122,6 +1195,7 @@ "keep": "เก็บ", "keep_all": "เก็บทั้งหมด", "keep_description": "เลือกสิ่งที่จะเก็บไว้บนอุปกรณ์ของคุณขณะเพิ่มพื้นที่ว่าง", + "keep_on_device_hint": "เลือกรายการที่จะเก็บไว้บนอุปกรณ์นี้", "keep_this_delete_others": "เก็บสิ่งนี้ไว้ ลบอันอื่นออก", "kept_this_deleted_others": "เก็บเนื้อหานี้และลบ {count, plural, one {# Asset} other {# Asset}}", "keyboard_shortcuts": "ปุ่มพิมพ์ลัด", @@ -1195,7 +1269,7 @@ "login_password_changed_error": "เกิดข้อผิดพลาดขณะเปลี่ยนรหัสผ่าน", "login_password_changed_success": "เปลี่ยนรหัสผ่านสำเร็จ", "logout_all_device_confirmation": "คุณต้องการออกจากระบบทุกอุปกรณ์ ใช่หรือไม่ ?", - "logout_this_device_confirmation": "คุณต้องการออกจากระบบใช่หรือไม่ ?", + "logout_this_device_confirmation": "คุณต้องการออกจากระบบบนอุปกรณ์นี้หรือไม่?", "longitude": "ลองจิจูด", "look": "ดู", "loop_videos": "วนวิดีโอ", @@ -1297,7 +1371,6 @@ "no_results_description": "ลองใช้คำพ้องหรือคำหลักที่กว้างกว่านี้", "no_shared_albums_message": "สร้างอัลบั้มเพื่อแชร์รูปภาพและวิดีโอกับคนในเครือข่ายของคุณ", "not_in_any_album": "ไม่อยู่ในอัลบั้มใด ๆ", - "note_apply_storage_label_to_previously_uploaded assets": "หมายเหตุ: หากต้องการใช้ป้ายกำกับพื้นที่เก็บข้อมูลกับเนื้อหาที่อัปโหลดก่อนหน้านี้ ให้เรียกใช้", "notes": "หมายเหตุ", "notification_permission_dialog_content": "เพื่อเปิดการแจ้งเตือน เข้าตั้งค่าแล้วกดอนุญาต", "notification_permission_list_tile_content": "อนุญาตการแจ้งเตือน", @@ -1310,6 +1383,7 @@ "offline": "ออฟไลน์", "ok": "ตกลง", "oldest_first": "เรียงเก่าสุดก่อน", + "on_this_device": "บนอุปกรณ์นี้", "onboarding": "การเริ่มต้นใช้งาน", "onboarding_privacy_description": "ฟีเจอร์ (ตัวเลือก) ต่อไปนี้ต้องอาศัยบริการภายนอก และสามารถปิดใช้งานได้ตลอดเวลาในการตั้งค่าการ", "onboarding_theme_description": "เลือกธีมสี คุณสามารถเปลี่ยนแปลงได้ในภายหลังในการตั้งค่าของคุณ", @@ -1377,7 +1451,8 @@ "permission_onboarding_permission_limited": "สิทธ์จำกัด เพื่อให้ Immich สำรองข้อมูลและจัดการคลังภาพได้ ตั้งค่าสิทธิเข้าถึงรูปภาพและวิดีโอ", "permission_onboarding_request": "Immich จำเป็นจะต้องได้รับสิทธิ์ดูรูปภาพและวิดีโอ", "person": "บุคคล", - "person_birthdate": "เกิดวัน{date}", + "person_age_years": "อายุ {years, plural, other {# ปี}}", + "person_birthdate": "เกิดเมื่อ {date}", "photo_shared_all_users": "ดูเหมือนว่าคุณได้แชร์รูปภาพของคุณกับผู้ใช้ทั้งหมด หรือคุณไม่มีผู้ใช้ใดที่จะแชร์ด้วย", "photos": "รูปภาพ", "photos_and_videos": "รูปภาพ และ วิดีโอ", @@ -1451,7 +1526,7 @@ "reassigned_assets_to_new_person": "มอบหมาย {count, plural, one {# สื่อ} other {# สื่อ}} ให้กับบุคคลใหม่", "reassing_hint": "มอบหมายสื่อที่เลือกให้กับบุคคลที่มีอยู่แล้ว", "recent": "ล่าสุด", - "recent-albums": "อัลบั้มล่าสุด", + "recent_albums": "อัลบั้มล่าสุด", "recent_searches": "การค้นหาล่าสุด", "recently_added_page_title": "เพิ่มล่าสุด", "refresh": "รีเฟรช", @@ -1585,8 +1660,8 @@ "server_endpoint": "ปลายทางเซิร์ฟเวอร์", "server_info_box_app_version": "เวอร์ชันแอพ", "server_info_box_server_url": "URL เซิร์ฟเวอร์", - "server_offline": "Server ออฟไลน์", - "server_online": "Server ออนไลน์", + "server_offline": "เซิร์ฟเวอร์ออฟไลน์", + "server_online": "เซิร์ฟเวอร์ออนไลน์", "server_privacy": "ความเป็นส่วนตัวเซิร์ฟเวอร์", "server_stats": "สถิติเซิร์ฟเวอร์", "server_version": "เวอร์ชันของเซิร์ฟเวอร์", @@ -1613,7 +1688,7 @@ "setting_notifications_single_progress_subtitle": "ข้อมูลความคืบหน้าการอัปโหลดโดยละเอียดต่อทรัพยากร", "setting_notifications_single_progress_title": "แสดงรายละเอียดสถานะการสำรองข้อมูลในเบื้องหลัง", "setting_notifications_subtitle": "ตั้งค่าการแจ้งเตือน", - "setting_notifications_total_progress_subtitle": "ความคืบหน้าการอัปโหลดโดยรวม (เสร็จสิ้น/ทรัพยากรทั้งหมด)", + "setting_notifications_total_progress_subtitle": "ความคืบหน้าการอัปโหลดโดยรวม (เสร็จสิ้น/สื่อทั้งหมด)", "setting_notifications_total_progress_title": "แสดงสถานะการสำรองข้อมูลในเบื้องหลังทั้งหมด", "setting_video_viewer_looping_title": "วนลูป", "settings": "ตั้งค่า", diff --git a/i18n/tr.json b/i18n/tr.json index a334ab789e..1331621ad9 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -115,13 +115,13 @@ "image_thumbnail_quality_description": "Küçük resim kalitesi 1-100 arasında. Daha yüksek değerler daha iyidir, ancak daha büyük dosyalar üretir ve uygulamanın yanıt hızını azaltabilir.", "image_thumbnail_title": "Küçük Fotoğraf Ayarları", "import_config_from_json_description": "JSON yapılandırma dosyası yükleyerek sistem yapılandırmasını içe aktar", - "job_concurrency": "{job} eş zamanlılık", + "job_concurrency": "{job} eşzamanlılık", "job_created": "Görev oluşturuldu", - "job_not_concurrency_safe": "Bu işlem eşzamanlama için uygun değil.", + "job_not_concurrency_safe": "Bu işlem eşzamanlılık açısından güvenli değil.", "job_settings": "Görev Ayarları", "job_settings_description": "Aynı anda çalışacak görevleri yönet", "jobs_delayed": "{jobCount, plural, other {# gecikmeli}}", - "jobs_failed": "{jobCount, plural, other {# Başarısız}}", + "jobs_failed": "{jobCount, plural, other {# başarısız}}", "jobs_over_time": "Zaman içinde işler", "library_created": "Oluşturulan kütüphane : {library}", "library_deleted": "Kütüphane silindi", @@ -140,7 +140,7 @@ "library_watching_settings": "Kütüphane izleme [DENEYSEL]", "library_watching_settings_description": "Değişen dosyalar için otomatik olarak izle", "logging_enable_description": "Günlüğü etkinleştir", - "logging_level_description": "Etkinleştirildiğinde hangi günlük seviyesi kullanılır.", + "logging_level_description": "Etkinleştirildiğinde, hangi günlük düzeyinin kullanılacağı.", "logging_settings": "Günlük Tutma", "machine_learning_availability_checks": "Kullanılabilirlik kontrolleri", "machine_learning_availability_checks_description": "Kullanılabilir makine öğrenimi sunucularını otomatik olarak algılayın ve tercih edin", @@ -163,23 +163,23 @@ "machine_learning_facial_recognition_model_description": "Modeller, azalan boyut sırasına göre listelenmiştir. Daha büyük modeller daha yavaştır ve daha fazla bellek kullanır, ancak daha iyi sonuçlar üretir. Bir modeli değiştirdikten sonra tüm görüntüler için yüz algılama işini yeniden çalıştırmanız gerektiğini unutmayın.", "machine_learning_facial_recognition_setting": "Yüz tanımayı etkinleştir", "machine_learning_facial_recognition_setting_description": "Devre dışı bırakıldığında fotoğraflar yüz tanıma için işlenmeyecek ve Keşfet sayfasındaki Kişiler sekmesini doldurmayacak.", - "machine_learning_max_detection_distance": "Maksimum tespit uzaklığı", + "machine_learning_max_detection_distance": "Maksimum algılama mesafesi", "machine_learning_max_detection_distance_description": "Resimleri birbirinin çifti saymak için hesap edilecek azami benzerlik ölçüsü, 0.001-0.1 aralığında. Daha yüksek değer daha hassas olup daha fazla çift tespit eder ancak çift olmayan resimleri birbirinin çifti sayabilir.", - "machine_learning_max_recognition_distance": "Maksimum tanıma uzaklığı", + "machine_learning_max_recognition_distance": "Maksimum tanıma mesafesi", "machine_learning_max_recognition_distance_description": "İki suretin aynı kişi olarak kabul edildiği azami benzerlik oranı; 0-2 aralığında bir değerdir. Düşük değerler iki farklı kişinin sehven aynı kişi olarak algılanmasını engeller ama aynı kişinin farklı pozlarının farklı suretler olarak algılanmasına sebep olabilir. İki sureti birleştirmek daha kolay olduğu için mümkün olduğunca düşük değerler seçin.", "machine_learning_min_detection_score": "Minimum tespit skoru", "machine_learning_min_detection_score_description": "Bir yüzün algılanması için gerekli asgari kararlılık miktarı; 0-1 aralığında bir değerdir. Düşük değerler daha fazla yüz tanır ama hatalı tanıma oranı artar.", - "machine_learning_min_recognized_faces": "Minimum tanınan yüzler", + "machine_learning_min_recognized_faces": "Tanınan minimum yüz sayısı", "machine_learning_min_recognized_faces_description": "Kişi oluşturulması için gereken minimum yüzler. Bu değeri yükseltmek yüz tanıma doğruluğunu arttırır fakat yüzün bir kişiye atanmama olasılığını arttırır.", "machine_learning_ocr": "OCR", "machine_learning_ocr_description": "Resimlerdeki metni tanımak için makine öğrenimini kullan", "machine_learning_ocr_enabled": "OCR'yi etkinleştir", "machine_learning_ocr_enabled_description": "Devre dışı bırakılırsa, resimler metin tanıma işleminden geçmeyecektir.", - "machine_learning_ocr_max_resolution": "En yüksek çözünürlük", + "machine_learning_ocr_max_resolution": "Maksimum çözünürlük", "machine_learning_ocr_max_resolution_description": "Bu çözünürlüğün üzerindeki önizlemeler, en-boy oranı korunarak yeniden boyutlandırılacaktır. Daha yüksek değerler daha doğru sonuç verir, ancak işlemesi daha uzun sürer ve daha fazla bellek kullanır.", - "machine_learning_ocr_min_detection_score": "En düşük tespit puanı", + "machine_learning_ocr_min_detection_score": "Minimum tespit puanı", "machine_learning_ocr_min_detection_score_description": "Metnin tespit edilmesi için minimum güven puanı 0-1 arasındadır. Düşük değerler daha fazla metin tespit eder, ancak yanlış pozitif sonuçlara yol açabilir.", - "machine_learning_ocr_min_recognition_score": "Minimum tespit puanı", + "machine_learning_ocr_min_recognition_score": "Minimum tanıma puanı", "machine_learning_ocr_min_score_recognition_description": "Algılanan metnin tanınması için minimum güven puanı 0-1 arasındadır. Daha düşük değerler daha fazla metni tanır, ancak yanlış pozitif sonuçlara neden olabilir.", "machine_learning_ocr_model": "OCR modeli", "machine_learning_ocr_model_description": "Sunucu modelleri mobil modellerden daha doğrudur, ancak işlenmesi daha uzun sürer ve daha fazla bellek kullanır.", @@ -243,7 +243,7 @@ "nightly_tasks_settings_description": "Gece görevlerini yönet", "nightly_tasks_start_time_setting": "Başlangıç saati", "nightly_tasks_start_time_setting_description": "Sunucunun gece görevlerini çalıştırmaya başladığı saat", - "nightly_tasks_sync_quota_usage_setting": "Kota kullanımını eşzamanla", + "nightly_tasks_sync_quota_usage_setting": "Kota kullanımını senkronize et", "nightly_tasks_sync_quota_usage_setting_description": "Mevcut kullanıma göre kullanıcı depolama kotasını güncelle", "no_paths_added": "Yol eklenmedi", "no_pattern_added": "Desen eklenmedi", @@ -272,7 +272,7 @@ "oauth_auto_register": "Otomatik kayıt", "oauth_auto_register_description": "OAuth ile giriş yapan yeni kullanıcıları otomatik kaydet", "oauth_button_text": "Buton yazısı", - "oauth_client_secret_description": "OAuth sağlayıcısı PKCE (Kod Değişimi İçin Kanıt Anahtarı) desteği sunmuyorsa gereklidir", + "oauth_client_secret_description": "Gizli istemci için veya genel istemci için PKCE (Kod Değişimi için Kanıt Anahtarı) desteklenmiyorsa gereklidir.", "oauth_enable_description": "OAuth ile giriş yap", "oauth_mobile_redirect_uri": "Mobil yönlendirme URL'si", "oauth_mobile_redirect_uri_override": "Mobilde zorla kullanılacak Yönlendirme Adresi", @@ -283,11 +283,11 @@ "oauth_settings_description": "OAuth giriş ayarlarını yönet", "oauth_settings_more_details": "Bu özellik hakkında daha fazla bilgi için bu sayfayı ziyaret edin Dökümanlar.", "oauth_storage_label_claim": "Depolama etiketi talebi", - "oauth_storage_label_claim_description": "Kullanıcının dosyalarını depolarken kullanılan alt klasörün adını belirlerken kulanılacak değer (en: OAuth claim).", + "oauth_storage_label_claim_description": "Kullanıcının depolama etiketini otomatik olarak bu talebin değerine ayarlayın.", "oauth_storage_quota_claim": "Depolama kotası talebi", - "oauth_storage_quota_claim_description": "Kullanıcıya depolama kotası koymak için kullanılacak değer (en: OAuth claim).", + "oauth_storage_quota_claim_description": "Kullanıcının depolama kotasını otomatik olarak bu talebin değerine ayarlayın.", "oauth_storage_quota_default": "Varsayılan depolama kotası (GiB)", - "oauth_storage_quota_default_description": "Değer (en: OAuth claim) mevcut değilse GiB cinsinden konulacak kota.", + "oauth_storage_quota_default_description": "Talepte bulunulmadığı durumlarda GiB cinsinden kullanılacak kota.", "oauth_timeout": "İstek Zaman Aşımı", "oauth_timeout_description": "Milisaniye cinsinden istek zaman aşımı", "ocr_job_description": "Resimlerdeki metni tanımak için makine öğrenimini kullan", @@ -321,7 +321,7 @@ "server_welcome_message_description": "Giriş sayfasında gösterilen mesaj.", "settings_page_description": "Yönetici ayarlar sayfası", "sidecar_job": "Ek dosya ile taşınan metadata", - "sidecar_job_description": "Dosya sisteminden yan araç meta verilerini keşfedin veya eşzamanlayın", + "sidecar_job_description": "Dosya sisteminden sidecar meta verilerini keşfedin veya senkronize edin", "slideshow_duration_description": "Her fotoğrafın kaç saniye görüntüleneceği", "smart_search_job_description": "Akıllı aramayı desteklemek için tüm öğelerde makine öğrenmesini çalıştırın", "storage_template_date_time_description": "Öğenin oluşturulma zaman damgası, tarih ve saat bilgisi için kullanılır", @@ -351,7 +351,7 @@ "template_settings": "Bildirim Şablonları", "template_settings_description": "Bildirim şablonlarını yönet", "theme_custom_css_settings": "Özel CSS", - "theme_custom_css_settings_description": "CSS (Cascading Style Sheets) kullanılarak Immich'in tasarımı değiştirilebilir.", + "theme_custom_css_settings_description": "Basamaklı Stil Sayfaları (css), Immich tasarımının özelleştirilmesine olanak tanır.", "theme_settings": "Tema Ayarları", "theme_settings_description": "Immich web arayüzünün özelleştirilmesi ayarlarını yönet", "thumbnail_generation_job": "Önizlemeleri oluştur", @@ -359,7 +359,7 @@ "transcoding_acceleration_api": "Hızlandırma API", "transcoding_acceleration_api_description": "Video formatı çevriminde kullanılacak API. Bu ayara 'mümkün olduğunca' uyulmaktadır; seçilen API'da sorun çıkarsa yazılım tabanlı çevirime dönülür. VP9 donanımınıza bağlı olarak çalışmayabilir.", "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU gerektirir)", - "transcoding_acceleration_qsv": "Hızlı Eşzamanlama (7. nesil veya daha yeni bir Intel CPU gerektirir)", + "transcoding_acceleration_qsv": "Hızlı Senkronizasyon (7. nesil Intel işlemci veya üzeri gerektirir)", "transcoding_acceleration_rkmpp": "RKMPP (Sadece Rockchip SOC'ler)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Kabul edilen ses kodekleri", @@ -375,7 +375,7 @@ "transcoding_codecs_learn_more": "Buradaki terminolojiyi öğrenmek için FFmpeg dokümantasyonlarına bakabilirsiniz: H.264, HEVC ve VP9.", "transcoding_constant_quality_mode": "Sabit kalite modu", "transcoding_constant_quality_mode_description": "ICQ, CQP'den daha iyidir, ancak bazı donanım hızlandırma cihazları bu modu desteklemez. Bu seçeneğin ayarlanması, kalite tabanlı kodlama kullanırken belirtilen modu tercih eder. ICQ'yu desteklemediği için NVENC tarafından göz ardı edilir.", - "transcoding_constant_rate_factor": "Sabit oran faktörü (-SOF)", + "transcoding_constant_rate_factor": "Sabit oran faktörü (-crf)", "transcoding_constant_rate_factor_description": "Video kalite seviyesi. Tipik değerler H.264 için 23, HEVC için 28, VP9 için 31 ve AV1 için 35'tir. Daha düşük değerler daha iyi kalite sağlar, ancak daha büyük dosyalar üretir.", "transcoding_disabled_description": "Videoları dönüştürmeyin, bazı istemcilerde oynatma bozulabilir", "transcoding_encoding_options": "Kodlama Seçenekleri", @@ -386,7 +386,7 @@ "transcoding_hardware_decoding_setting_description": "Uçtan uca hızlandırmayı, sadece kodlamayı hızlandırmanın yerine etkinleştirir. Tüm videolarda çalışmayabilir.", "transcoding_max_b_frames": "Maksimum B-kareler", "transcoding_max_b_frames_description": "Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. Eski cihazlarda donanım hızlandırma ile uyumlu olmayabilir. 0, B-çerçevelerini devre dışı bırakır, -1 ise bu değeri otomatik olarak ayarlar.", - "transcoding_max_bitrate": "Maksimum bitrate", + "transcoding_max_bitrate": "Maksimum bit hızı", "transcoding_max_bitrate_description": "Maksimum bit hızı ayarlamak, kaliteyi az bir maliyetle düşürerek dosya boyutlarını daha öngörülebilir hale getirebilir. 720p çözünürlükte, tipik değerler VP9 veya HEVC için 2600 kbit/s, H.264 için ise 4500 kbit/s’dir. 0 olarak ayarlanırsa devre dışı bırakılır. Birim belirtilmediğinde, k (kbit/s için) varsayılır; bu nedenle 5000, 5000k ve 5M (Mbit/s için) eşdeğerdir.", "transcoding_max_keyframe_interval": "Maksimum ana kare aralığı", "transcoding_max_keyframe_interval_description": "Ana kareler arasındaki maksimum kare mesafesini ayarlar. Düşük değerler sıkıştırma verimliliğini kötüleştirir, ancak arama sürelerini iyileştirir ve hızlı hareket içeren sahnelerde kaliteyi artırabilir. 0 bu değeri otomatik olarak ayarlar.", @@ -466,7 +466,7 @@ "advanced_settings_self_signed_ssl_subtitle": "Sunucu uç noktası için SSL sertifika doğrulamasını atlar. Kendinden imzalı sertifikalar için gereklidir.", "advanced_settings_self_signed_ssl_title": "Kendinden imzalı SSL sertifikalarına izin ver [DENEYSEL]", "advanced_settings_sync_remote_deletions_subtitle": "Web üzerinde işlem yapıldığında, bu aygıttaki öğeyi otomatik olarak sil veya geri yükle", - "advanced_settings_sync_remote_deletions_title": "Uzaktan silmeleri eşzamanla [DENEYSEL]", + "advanced_settings_sync_remote_deletions_title": "Uzaktan silme işlemlerini senkronize et [DENEYSEL]", "advanced_settings_tile_subtitle": "Gelişmiş kullanıcı ayarları", "advanced_settings_troubleshooting_subtitle": "Sorun giderme için ek özellikleri etkinleştirin", "advanced_settings_troubleshooting_title": "Sorun Giderme", @@ -573,8 +573,8 @@ "asset_list_settings_subtitle": "Fotoğraf ızgara düzeni ayarları", "asset_list_settings_title": "Fotoğraf Izgarası", "asset_not_found_on_device_android": "Cihazda varlık bulunamadı", - "asset_not_found_on_device_ios": "Cihazınızda varlık bulunamadı. eğer icloud kullanıyorsanız, icloud'da depolanan dosyanın hatalı olması nedeniyle varlığa erişilemeyebilir.", - "asset_not_found_on_icloud": "Varlık icloud'da bulunamadı. İcloud'da depolanan dosyanın hatalı olması nedeniyle varlığa erişilemeyebilir.", + "asset_not_found_on_device_ios": "Cihazda öğe bulunamadı. iCloud kullanıyorsanız, iCloud platformunda depolanan hatalı dosya nedeniyle öğeye erişilemeyebilir", + "asset_not_found_on_icloud": "Öğe iCloud platformunda bulunamadı. Öğe, iCloud platformunda depolanan hatalı dosya nedeniyle erişilemeyebilir", "asset_offline": "Öğe Çevrim Dışı", "asset_offline_description": "Bu harici öğe artık diskte bulunmuyor. Yardım için lütfen Immich yöneticinizle iletişime geçin.", "asset_restored_successfully": "Öğe başarıyla geri yüklendi", @@ -626,7 +626,7 @@ "backup_album_selection_page_select_albums": "Albümleri seç", "backup_album_selection_page_selection_info": "Seçim Bilgileri", "backup_album_selection_page_total_assets": "Toplam eşsiz öğeler", - "backup_albums_sync": "Albüm Senkronizasyonunu Yedekle", + "backup_albums_sync": "Albüm Yedekleme Senkronizasyonu", "backup_all": "Tümü", "backup_background_service_backup_failed_message": "Yedekleme başarısız. Tekrar deneniyor…", "backup_background_service_complete_notification": "Öğe yedekleme tamamlandı", @@ -766,7 +766,7 @@ "cleanup_found_assets": "{count} adet yedeklenmiş görsel bulundu", "cleanup_found_assets_with_size": "{count} yedeklenmiş öğe bulundu ({size})", "cleanup_icloud_shared_albums_excluded": "iCloud Paylaşılan Albümleri tarama kapsamı dışında tutulmuştur", - "cleanup_no_assets_found": "Yukarıdaki ölçütlere uyan hiçbir öğe bulunamadı. Alan Aç yalnızca sunucuya yedeklenmiş öğeleri kaldırabilir.", + "cleanup_no_assets_found": "Yukarıdaki kriterlere uyan hiçbir varlık bulunamadı. Alan Boşaltma, yalnızca sunucuya yedeklenmiş varlıkları kaldırabilir", "cleanup_preview_title": "Silinecek görseller ({count})", "cleanup_step3_description": "Tarih ve saklama ayarlarınıza uyan, yedeklenmiş öğeleri tarayın.", "cleanup_step4_summary": "Yerel cihazınızdan kaldırılacak {count} öğe ({date} tarihinden önce oluşturulmuş). Fotoğraflara Immich uygulaması üzerinden erişmeye devam edebilirsiniz.", @@ -782,6 +782,8 @@ "client_cert_import": "İçe Aktar", "client_cert_import_success_msg": "İstemci sertifikası içe aktarıldı", "client_cert_invalid_msg": "Geçersiz sertifika dosyası veya yanlış şifre", + "client_cert_password_message": "Bu sertifika için şifreyi girin", + "client_cert_password_title": "Sertifika Şifresi", "client_cert_remove_msg": "İstemci sertifikası kaldırıldı", "client_cert_subtitle": "Yalnızca PKCS12 (.p12, .pfx) formatını destekler. Sertifika içe aktarma/kaldırma işlemi yalnızca oturum açmadan önce yapılabilir", "client_cert_title": "SSL istemci sertifikası [DENEYSEL]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "Değişiklikler kaydedilmeyecek", "editor_close_without_save_title": "Düzenleyici kapatılsın mı?", "editor_confirm_reset_all_changes": "Tüm değişikleri iptal edilecek. Emin misiniz?", + "editor_discard_edits_confirm": "Düzenlemeleri iptal et", + "editor_discard_edits_prompt": "Kaydedilmemiş düzenlemeleriniz var. Bunları iptal etmek istediğinizden emin misiniz?", + "editor_discard_edits_title": "Düzenlemeleri iptal edelim mi?", + "editor_edits_applied_error": "Düzenlemeler uygulanamadı", + "editor_edits_applied_success": "Düzenlemeler başarıyla uygulandı", "editor_flip_horizontal": "Yatay çevir", "editor_flip_vertical": "Dikey çevir", "editor_orientation": "Yönlendirme", @@ -1152,7 +1159,7 @@ }, "errors_text": "Hatalar", "exclusion_pattern": "Hariç tutma modeli", - "exif": "EXIF", + "exif": "Exif", "exif_bottom_sheet_description": "Açıklama Ekle...", "exif_bottom_sheet_description_error": "Açıklama güncelleme hatası", "exif_bottom_sheet_details": "DETAYLAR", @@ -1196,8 +1203,10 @@ "features_in_development": "Geliştirme Aşamasındaki Özellikler", "features_setting_description": "Uygulamanın özelliklerini yönet", "file_name_or_extension": "Dosya adı veya uzantı", + "file_name_text": "Dosya adı", + "file_name_with_value": "Dosya adı: {file_name}", "file_size": "Dosya boyutu", - "filename": "Dosya adı", + "filename": "Dosya Adı", "filetype": "Dosya tipi", "filter": "Filtre", "filter_description": "Hedef öğeleri filtreleme koşulları", @@ -1321,9 +1330,9 @@ "invite_people": "Kişileri Davet Et", "invite_to_album": "Albüme davet et", "ios_debug_info_fetch_ran_at": "Veri çekme {dateTime} tarihinde çalıştırıldı", - "ios_debug_info_last_sync_at": "Son eşzamanlama {dateTime}", + "ios_debug_info_last_sync_at": "Son senkronizasyon {dateTime}", "ios_debug_info_no_processes_queued": "Hiçbir arka plan işlemi kuyruğa alınmadı", - "ios_debug_info_no_sync_yet": "Henüz arka plan eşzamanlama görevi çalıştırılmadı", + "ios_debug_info_no_sync_yet": "Henüz hiçbir arka plan senkronizasyon görevi çalıştırılmadı", "ios_debug_info_processes_queued": "{count, plural, one {{count} arka plan işlemi kuyruğa alındı} other {{count} arka plan işlemi kuyruğa alındı}}", "ios_debug_info_processing_ran_at": "İşleme {dateTime} tarihinde çalıştırıldı", "items_count": "{count, plural, one {# Öğe} other {# Öğe}}", @@ -1582,7 +1591,7 @@ "no_configuration_needed": "Yapılandırmaya gerek yok", "no_devices": "Yetkili cihaz yok", "no_duplicates_found": "Hiçbir kopya bulunamadı.", - "no_exif_info_available": "EXIF bilgisi mevcut değil", + "no_exif_info_available": "Exif bilgisi mevcut değil", "no_explore_results_message": "Koleksiyonunuzu keşfetmek için daha fazla fotoğraf yükleyin.", "no_favorites_message": "En sevdiğiniz fotoğraf ve videoları hızlıca bulmak için favorilere ekleyin", "no_filters_added": "Henüz filtre eklenmedi", @@ -1604,7 +1613,6 @@ "not_available": "YOK", "not_in_any_album": "Hiçbir albümde değil", "not_selected": "Seçilmedi", - "note_apply_storage_label_to_previously_uploaded assets": "Not: Daha önce yüklenen öğeler için bir depolama yolu etiketi uygulamak üzere şunu başlatın", "notes": "Notlar", "nothing_here_yet": "Burada henüz bir şey yok", "notification_permission_dialog_content": "Bildirimleri etkinleştirmek için cihaz ayarlarına gidin ve izin verin.", @@ -1640,7 +1648,7 @@ "options": "Seçenekler", "or": "veya", "organize_into_albums": "Albümler halinde düzenle", - "organize_into_albums_description": "Mevcut eşzamanlama ayarlarını kullanarak mevcut fotoğrafları albümlere ekleyin", + "organize_into_albums_description": "Mevcut fotoğrafları geçerli senkronizasyon ayarlarını kullanarak albümlere yerleştirin", "organize_your_library": "Kütüphanenizi düzenleyin", "original": "orijinal", "other": "Diğer", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# öğe} other {# öğeler}} yeni bir kişiye atandı", "reassing_hint": "Seçili öğeleri mevcut bir kişiye atayın", "recent": "Son", - "recent-albums": "Son kaydedilen albümler", + "recent_albums": "Son kaydedilen albümler", "recent_searches": "Son aramalar", "recently_added": "Son eklenenler", "recently_added_page_title": "Son Eklenenler", @@ -1867,7 +1875,7 @@ "reset_pin_code_success": "PIN kodu başarıyla sıfırlandı", "reset_pin_code_with_password": "PIN kodunuzu her zaman şifrenizle sıfırlayabilirsiniz", "reset_sqlite": "SQLite Veritabanını Sıfırla", - "reset_sqlite_confirmation": "SQLite veritabanını sıfırlamak istediğinizden emin misiniz? Verileri yeniden eşzamanlamak için oturumu kapatıp tekrar oturum açmanız gerekecektir", + "reset_sqlite_confirmation": "SQLite veritabanını sıfırlamak istediğinizden emin misiniz? Verileri yeniden senkronize etmek için oturumu kapatıp tekrar giriş yapmanız gerekecek", "reset_sqlite_success": "SQLite veritabanını başarıyla sıfırladınız", "reset_to_default": "Varsayılana sıfırla", "resolution": "Çözünürlük", @@ -2072,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} yıl", "shared_link_edit_password_hint": "Paylaşım şifresini girin", "shared_link_edit_submit_button": "Bağlantıyı güncelle", - "shared_link_error_server_url_fetch": "Sunucu URL'si alınamadı", + "shared_link_error_server_url_fetch": "Sunucu url'si alınamıyor", "shared_link_expires_day": "Süresi {count} gün içinde doluyor", "shared_link_expires_days": "Süresi {count} gün içinde doluyor", "shared_link_expires_hour": "Süresi {count} saat içinde doluyor", @@ -2176,13 +2184,13 @@ "support_and_feedback": "Destek & Geri Bildirim", "support_third_party_description": "Immich kurulumu üçüncü bir tarafça yapıldı. Yaşadığınız sorunlar bu paketle ilgili olabilir. Lütfen öncelikli olarak aşağıdaki bağlantıları kullanarak bu sağlayıcıyla iletişime geçin.", "swap_merge_direction": "Birleştirme yönünü değiştir", - "sync": "Eşzamanla", - "sync_albums": "Albümleri eşzamanla", - "sync_albums_manual_subtitle": "Yüklenmiş fotoğraf ve videoları yedekleme için seçili albümler ile eşzamanlayın", - "sync_local": "Yerel Eşzamanlama", - "sync_remote": "Uzaktan Eşzamanlama", - "sync_status": "Eşzamanlama Durumu", - "sync_status_subtitle": "Eşzamanlama sistemini görüntüleyin ve yönetin", + "sync": "Senkronizasyon", + "sync_albums": "Albümleri senkronize et", + "sync_albums_manual_subtitle": "Yüklenen tüm videoları ve fotoğrafları seçilen yedekleme albümlerine senkronize edin", + "sync_local": "Yerel Senkronizasyon", + "sync_remote": "Uzaktan Senkronizasyon", + "sync_status": "Senkronizasyon Durumu", + "sync_status_subtitle": "Senkronizasyon sistemini görüntüleyin ve yönetin", "sync_upload_album_setting_subtitle": "Fotoğraflarınızı ve videolarınızı oluşturun ve Immich'te seçtiğiniz albümlere yükleyin", "tag": "Etiket", "tag_assets": "Öğeleri etiketle", @@ -2297,6 +2305,7 @@ "upload_details": "Yükleme Ayrıntıları", "upload_dialog_info": "Seçili öğeleri sunucuya yedeklemek istiyor musunuz?", "upload_dialog_title": "Öğe Yükle", + "upload_error_with_count": "{count, plural, one {# öğe} other {# öğeler}} için yükleme hatası", "upload_errors": "{count, plural, one {# hata} other {# hatayla}} yükleme tamamlandı, yeni yüklenen öğeleri görmek için sayfayı güncelleyin.", "upload_finished": "Yükleme tamamlandı", "upload_progress": "{remaining, number} kalan - {processed, number}/{total, number} işlendi", diff --git a/i18n/uk.json b/i18n/uk.json index 6834d22fc7..0609edf28c 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -782,6 +782,8 @@ "client_cert_import": "Імпорт", "client_cert_import_success_msg": "Клієнтський сертифікат імпортовано", "client_cert_invalid_msg": "Недійсний файл сертифіката або неправильний пароль", + "client_cert_password_message": "Введіть пароль для цього сертифіката", + "client_cert_password_title": "Пароль сертифіката", "client_cert_remove_msg": "Клієнтський сертифікат видалено", "client_cert_subtitle": "Підтримує лише формат PKCS12 (.p12, .pfx). Імпорт/видалення сертифіката доступне лише перед входом у систему", "client_cert_title": "SSL-сертифікат клієнта [ЕКСПЕРИМЕНТАЛЬНИЙ]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "Зміни не будуть збережені", "editor_close_without_save_title": "Закрити редактор?", "editor_confirm_reset_all_changes": "Ви впевнені, що хочете скинути всі зміни?", + "editor_discard_edits_confirm": "Скасувати зміни", + "editor_discard_edits_prompt": "У вас є незбережені зміни. Ви впевнені, що хочете їх скасувати?", + "editor_discard_edits_title": "Скасувати зміни?", + "editor_edits_applied_error": "Не вдалося застосувати зміни", + "editor_edits_applied_success": "Зміни успішно застосовано", "editor_flip_horizontal": "Відобразити горизонтально", "editor_flip_vertical": "Відобразити вертикально", "editor_orientation": "Орієнтація", @@ -1196,6 +1203,8 @@ "features_in_development": "Функції в розробці", "features_setting_description": "Керування додатковими можливостями застосунку", "file_name_or_extension": "Ім'я файлу або розширення", + "file_name_text": "Ім'я файлу", + "file_name_with_value": "Ім'я файлу: {file_name}", "file_size": "Розмір файлу", "filename": "Ім'я файлу", "filetype": "Тип файлу", @@ -1604,7 +1613,6 @@ "not_available": "Немає даних", "not_in_any_album": "У жодному альбомі", "not_selected": "Не вибрано", - "note_apply_storage_label_to_previously_uploaded assets": "Примітка: Щоб застосувати мітку сховища до раніше вивантажених файлів, виконайте команду", "notes": "Нотатки", "nothing_here_yet": "Тут ще нічого немає", "notification_permission_dialog_content": "Щоб увімкнути сповіщення, перейдіть до Налаштувань і надайте дозвіл.", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "Перепризначено {count, plural, one {# файл} few {# файли} many {# файлів} other {# файлів}} новій особі", "reassing_hint": "Призначити обрані файли існуючій особі", "recent": "Нещодавно", - "recent-albums": "Останні альбоми", + "recent_albums": "Останні альбоми", "recent_searches": "Нещодавні пошукові запити", "recently_added": "Нещодавно додані", "recently_added_page_title": "Нещодавні", diff --git a/i18n/vi.json b/i18n/vi.json index ec1b497449..0ba340ad2c 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -562,7 +562,7 @@ "asset_uploaded": "Đã tải lên", "asset_uploading": "Đang tải lên…", "asset_viewer_settings_subtitle": "Cách thư viện hiển thị", - "asset_viewer_settings_title": "Trình xem tệp", + "asset_viewer_settings_title": "Trình xem ảnh", "assets": "Tệp", "assets_added_count": "Đã thêm {count, plural, one {# tệp} other {# tệp}}", "assets_added_to_album_count": "Đã thêm {count, plural, one {# tệp} other {# tệp}} vào album", @@ -1516,7 +1516,6 @@ "not_available": "Thiếu", "not_in_any_album": "Không thuộc album nào", "not_selected": "Không được chọn", - "note_apply_storage_label_to_previously_uploaded assets": "Lưu ý: Để áp dụng Nhãn lưu trữ cho các ảnh đã tải lên trước đó, hãy chạy", "notes": "Lưu ý", "nothing_here_yet": "Chưa có nội dung nào", "notification_permission_dialog_content": "Để bật thông báo, chuyển tới Cài đặt và chọn cho phép.", @@ -1717,7 +1716,7 @@ "reassigned_assets_to_new_person": "Đã gán lại {count, plural, one {# ảnh} other {# ảnh}} cho một người mới", "reassing_hint": "Gán các ảnh đã chọn cho một người hiện có", "recent": "Gần đây", - "recent-albums": "Album gần đây", + "recent_albums": "Album gần đây", "recent_searches": "Tìm kiếm gần đây", "recently_added": "Thêm gần đây", "recently_added_page_title": "Mới thêm gần đây", @@ -1923,7 +1922,7 @@ "setting_image_viewer_preview_title": "Tải ảnh xem trước", "setting_image_viewer_title": "Ảnh", "setting_languages_apply": "Áp dụng", - "setting_languages_subtitle": "Ngôn ngữ app", + "setting_languages_subtitle": "Thay đổi ngôn ngữ ứng dụng", "setting_notifications_notify_failures_grace_period": "Thông báo lỗi sao lưu nền: {duration}", "setting_notifications_notify_hours": "{count} giờ", "setting_notifications_notify_immediately": "ngay lập tức", diff --git a/i18n/yue_Hant.json b/i18n/yue_Hant.json index 823149da9e..372816da2a 100644 --- a/i18n/yue_Hant.json +++ b/i18n/yue_Hant.json @@ -5,7 +5,7 @@ "acknowledge": "了解", "action": "動作", "action_common_update": "更新", - "action_description": "針對篩選後嘅資源執行嘅", + "action_description": "針對篩選後嘅資源執行嘅一系列動作", "actions": "動作", "active": "正在處理", "active_count": "正在處理:{count}", @@ -21,6 +21,7 @@ "add_assets": "加資源", "add_birthday": "加一個生日", "add_endpoint": "加端點", + "add_exclusion_pattern": "加入篩選條件", "add_filter": "加過濾器", "add_filter_description": "點擊以加一個過濾條件", "add_location": "加位置", @@ -34,19 +35,37 @@ "add_to_album_bottom_sheet_added": "已加至{album}", "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", "add_to_album_bottom_sheet_some_local_assets": "無法加部分本機資源至相簿", + "add_to_album_toggle": "選擇{album}相簿", "add_to_albums": "加至相簿", "add_to_albums_count": "加 ({count}) 個項目至相簿", "add_to_bottom_bar": "加至", "add_to_shared_album": "加至共享相簿", "add_url": "加網址", + "add_workflow_step": "增加工作步驟", "added_to_favorites": "已加至最愛", "added_to_favorites_count": "已加{count, number} 個項目至最愛", "admin": { "admin_user": "管理員用戶", "authentication_settings": "驗證設定", "authentication_settings_description": "管理密碼、OAuth 同其他驗證設定", + "background_task_job": "背景操作", + "backup_database": "建立資料庫備份", + "backup_database_enable_description": "啟用資料庫備份", + "backup_keep_last_amount": "保留先前備份嘅數量", + "backup_onboarding_1_description": "係雲端或者其他實體地方建立嘅備份副本。", + "backup_onboarding_2_description": "儲存係唔同裝置嘅本地副本。包含主要嘅檔案同埋喺本機嘅備份。", + "backup_onboarding_3_description": "資料包含原始文件,總共備份嘅次數。呢個包括1份異地嘅備份同埋2份本機副本。", + "backup_onboarding_footer": "有關其他Immich備份嘅資料,請參考。", "backup_onboarding_parts_title": "一個3-2-1備份包括:", - "backup_onboarding_title": "備份" + "backup_onboarding_title": "備份", + "backup_settings": "資料庫備份嘅設定", + "backup_settings_description": "管理資料庫備份嘅設定。", + "cleared_jobs": "已清除{job}嘅工作", + "config_set_by_file": "依家嘅設定係由設定檔案訂立嘅", + "confirm_delete_library": "你係唔係確定要鏟除{library}嘅外部資料庫?", + "confirm_delete_library_assets": "你係唔係確定要鏟除呢個外部媒體庫?Immich將會鏟除 {count, plural, one {# 個項目} other {# 個項目}},且無法撤回。檔案仍然會被保留喺硬碟入面。", + "confirm_email_below": "請喺底下輸入{email}已確認", + "confirm_reprocess_all_faces": "你確定要重新處理所有嘅面貌?呢個過程亦都會清除已命名嘅人物。" }, "main_menu": "主選單", "maintenance_action_restore": "還原緊數據庫", diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_Hans.json similarity index 96% rename from i18n/zh_SIMPLIFIED.json rename to i18n/zh_Hans.json index 23f3ea4eec..2e7960bffd 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_Hans.json @@ -5,7 +5,7 @@ "acknowledge": "已知悉", "action": "操作", "action_common_update": "更新", - "action_description": "针对筛选出的资源要执行的一组操作", + "action_description": "对筛选出的资产执行的一组操作", "actions": "操作", "active": "进行中", "active_count": "活动: {count}", @@ -18,7 +18,7 @@ "add_a_title": "添加标题", "add_action": "添加操作", "add_action_description": "点击以添加要执行的操作", - "add_assets": "添加资源", + "add_assets": "添加资产", "add_birthday": "添加生日", "add_endpoint": "添加端点", "add_exclusion_pattern": "添加排除规则", @@ -34,7 +34,7 @@ "add_to_album": "添加到相册", "add_to_album_bottom_sheet_added": "已添加至 {album}", "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", - "add_to_album_bottom_sheet_some_local_assets": "部分本地资源无法添加到相册", + "add_to_album_bottom_sheet_some_local_assets": "部分本地资产无法添加到相册", "add_to_album_toggle": "切换 {album} 的选中状态", "add_to_albums": "添加到相册", "add_to_albums_count": "添加到相册 ({count})", @@ -85,11 +85,11 @@ "exclusion_pattern_description": "排除规则允许您在扫描资产库时忽略特定的文件和文件夹。如果您有某些包含不希望导入的文件(例如 RAW 格式文件)的文件夹,此功能将非常有用。", "export_config_as_json_description": "将当前系统配置下载为 JSON 文件", "external_libraries_page_description": "管理外部资产库", - "face_detection": "人脸识别", - "face_detection_description": "使用机器学习检测资源中的人脸,对于视频仅处理其缩略图;“刷新”会重新处理所有资源,“重置”会清除所有当前的人脸数据,“缺失”则仅将尚未处理的资源加入队列;人脸检测完成后,检测到的人脸将自动加入人物识别队列,系统会将其归入现有或新建的人物分组中。", - "facial_recognition_job_description": "将检测到的人脸归类为不同的人物。此步骤在“人脸检测”完成后运行。“重置”会(重新)聚类所有人脸。“缺失”则将尚未分配人物的人脸加入队列。", + "face_detection": "人脸检测", + "face_detection_description": "使用机器学习检测影像中的人脸,对于视频仅处理其缩略图。“刷新”会重新处理所有影像;“重置”会清除当前所有人脸数据;“缺失”则仅将未曾处理过的影像加入队列。当“人脸检测”完成后,系统会将新检测到的人脸放入“人脸识别”队列,以将其归类到现有或新建的人物分组中。", + "facial_recognition_job_description": "将检测到的人脸归类为不同的人物,此步骤需在“人脸检测”完成后运行。“重置”会(重新)聚类所有人脸。“缺失”则将尚未确定是谁的人脸加入归类队列。", "failed_job_command": "命令 {command} 在执行任务 {job} 时失败", - "force_delete_user_warning": "警告:此操作将立即删除该用户及其所有资源。此操作不可撤销,且文件无法恢复。", + "force_delete_user_warning": "警告:此操作将立即删除该用户及其所有资产。此操作不可撤销,且文件无法恢复。", "image_format": "格式", "image_format_description": "WebP 格式的文件体积比 JPEG 更小,但编码速度较慢。", "image_fullsize_description": "已剥离元数据的全尺寸图像,放大查看时使用", @@ -98,20 +98,20 @@ "image_fullsize_quality_description": "全尺寸图像质量(1-100)。数值越高画质越好,但生成的文件也越大。", "image_fullsize_title": "全尺寸图像设置", "image_prefer_embedded_preview": "优先使用嵌入式预览", - "image_prefer_embedded_preview_setting_description": "使用 RAW 照片中的嵌入式预览作为图像处理的源文件(如果存在)。这能为部分图像生成更准确的色彩,但预览图的质量取决于相机,且图像可能包含更多的压缩伪影。", + "image_prefer_embedded_preview_setting_description": "使用 RAW 文件内嵌入的预览图像(如有)作为后续处理的基础。这能为部分图像生成更准确的色彩,但嵌入式预览图像的质量取决于相机型号,且可能因有损压缩包含更多的伪影。", "image_prefer_wide_gamut": "优先使用广色域", - "image_prefer_wide_gamut_setting_description": "缩略图使用 Display P3 色彩空间。这能更好地保留广色域图像的色彩鲜艳度,但在使用旧版浏览器的老旧设备上,图像显示效果可能有所不同。sRGB 图像将保持为 sRGB,以避免色彩偏移。", - "image_preview_description": "已剥离元数据的中等尺寸图像,用于查看单个资产时以及机器学习功能", + "image_prefer_wide_gamut_setting_description": "缩略图使用 Display P3 色彩空间。这能更好地保留广色域图像的色彩鲜艳度,但在使用旧版浏览器的老旧设备上,可能导致色差。sRGB 图像将保持为 sRGB,以避免色彩偏移。", + "image_preview_description": "已剥离元数据的中等尺寸图像,用于单幅影像展示及机器学习", "image_preview_quality_description": "预览图质量(1-100)。数值越高画质越好,但生成的文件越大,且可能降低应用响应速度。设置过低的数值可能影响机器学习(识别)的准确度。", - "image_preview_title": "预览设置", - "image_progressive": "逐步", - "image_progressive_description": "对 JPEG 图像进行逐步编码,以实现渐进式加载显示。这不会影响 WebP 图像。", + "image_preview_title": "预览图设置", + "image_progressive": "渐进式编码", + "image_progressive_description": "对 JPEG 图像进行渐进式编码,以提升图片加载体验。此开关对 WebP 无效。", "image_quality": "质量", "image_resolution": "分辨率", "image_resolution_description": "较高的分辨率能保留更多图像细节,但编码时间更长、生成的文件更大,且可能导致应用响应变慢。", "image_settings": "图像设置", "image_settings_description": "管理生成图像的质量和分辨率", - "image_thumbnail_description": "已剥离元数据的小型缩略图,用于查看主时间线等照片组时显示", + "image_thumbnail_description": "已剥离元数据的小型缩略图,用于列表展示多张照片,例如主时间线", "image_thumbnail_quality_description": "缩略图质量(1-100)。数值越高画质越好,但生成的文件越大,且可能降低应用响应速度。", "image_thumbnail_title": "缩略图设置", "import_config_from_json_description": "通过上传 JSON 配置文件导入系统配置", @@ -122,7 +122,7 @@ "job_settings_description": "管理任务并发数", "jobs_delayed": "{jobCount, plural, other {# 个延迟}}", "jobs_failed": "{jobCount, plural, other {# 个失败}}", - "jobs_over_time": "任务执行趋势", + "jobs_over_time": "任务动态", "library_created": "已创建资产库:{library}", "library_deleted": "资产库已删除", "library_details": "资产库详情", @@ -134,7 +134,7 @@ "library_scanning_enable_description": "开启定期扫描", "library_settings": "外部资产库", "library_settings_description": "管理外部资产库设置", - "library_tasks_description": "扫描外部资产库以获取新增/变更的资产", + "library_tasks_description": "扫描外部资产库以查找新增和变更的文件", "library_updated": "资产库已更新", "library_watching_enable_description": "监控外部资产库的文件变更", "library_watching_settings": "资产库监控 [实验性功能]", @@ -153,10 +153,10 @@ "machine_learning_clip_model_description": "在 此处 列出的 CLIP 模型名称。请注意,更改模型后,必须重新运行所有图片的“智能搜索”任务。", "machine_learning_duplicate_detection": "重复项检测", "machine_learning_duplicate_detection_enabled": "启用重复项检测", - "machine_learning_duplicate_detection_enabled_description": "若关闭此功能,完全相同的资源仍会被去重处理。", + "machine_learning_duplicate_detection_enabled_description": "若关闭此功能,完全相同的资产仍会被去重处理。", "machine_learning_duplicate_detection_setting_description": "利用 CLIP 嵌入向量识别潜在的重复项", "machine_learning_enabled": "启用机器学习", - "machine_learning_enabled_description": "若关闭此功能,所有机器学习相关特性将失效,且不受下方具体设置的影响。", + "machine_learning_enabled_description": "若关闭此处总开关,所有机器学习相关特性将全部停用,下方具体设置无效。", "machine_learning_facial_recognition": "人脸识别", "machine_learning_facial_recognition_description": "检测、识别并自动归类图片中的人脸", "machine_learning_facial_recognition_model": "人脸识别模型", @@ -171,10 +171,10 @@ "machine_learning_min_detection_score_description": "人脸检测的最低置信度分数,取值范围为 0-1。数值越低,检测到的人脸越多,但可能出现误判(例如将非人脸区域识别为人脸)。", "machine_learning_min_recognized_faces": "最小识别数量", "machine_learning_min_recognized_faces_description": "创建人物所需的最少人脸数量。调高此值可提升人脸识别的精准度,但会增加人脸无法被分配给人物的风险。", - "machine_learning_ocr": "OCR", + "machine_learning_ocr": "文字识别(OCR)", "machine_learning_ocr_description": "利用机器学习技术识别图片中的文本内容", - "machine_learning_ocr_enabled": "启用 OCR", - "machine_learning_ocr_enabled_description": "若禁用,图片将不会进行文字识别。", + "machine_learning_ocr_enabled": "启用文字识别(OCR)", + "machine_learning_ocr_enabled_description": "若禁用,将不会尝试识别图片中的文字。", "machine_learning_ocr_max_resolution": "最大分辨率", "machine_learning_ocr_max_resolution_description": "超过此分辨率的预览图将按比例调整大小。数值越高,效果越精准,但处理时间更长且更占用内存。", "machine_learning_ocr_min_detection_score": "最低检测阈值", @@ -232,7 +232,7 @@ "migration_job": "迁移", "migration_job_description": "将媒体文件和人脸的缩略图迁移到最新的文件夹结构", "nightly_tasks_cluster_faces_setting_description": "对新检测到的人脸运行人脸识别", - "nightly_tasks_cluster_new_faces_setting": "聚类新人脸", + "nightly_tasks_cluster_new_faces_setting": "聚类新检测到的人脸", "nightly_tasks_database_cleanup_setting": "数据库清理任务", "nightly_tasks_database_cleanup_setting_description": "清理数据库中过期的旧数据", "nightly_tasks_generate_memories_setting": "生成回忆", @@ -300,7 +300,7 @@ "queues": "任务队列", "queues_page_description": "管理任务队列页面", "quota_size_gib": "配额大小(GiB)", - "refreshing_all_libraries": "正在刷新所有库", + "refreshing_all_libraries": "刷新所有库", "registration": "管理员注册", "registration_description": "由于您是系统的第一位用户,系统将自动为您分配管理员权限。您需要负责相关的管理任务,后续的其他用户也将由您来创建。", "remove_failed_jobs": "移除失败任务", @@ -312,8 +312,8 @@ "send_welcome_email": "发送欢迎邮件", "server_external_domain_settings": "外部域名", "server_external_domain_settings_description": "公开分享链接的域名,需包含 http(s)://", - "server_public_users": "公开用户", - "server_public_users_description": "所有用户(姓名和邮箱)在将用户添加到共享相册时都会显示。关闭此功能后,用户列表将仅对管理员可见。", + "server_public_users": "用户公开", + "server_public_users_description": "在将用户添加到共享相册时,所有用户(姓名和邮箱)都会被列出。若关闭此功能,用户列表将仅对管理员可见。", "server_settings": "服务器设置", "server_settings_description": "管理服务器设置", "server_stats_page_description": "管理服务器统计页面", @@ -323,15 +323,15 @@ "sidecar_job": "附属元数据", "sidecar_job_description": "从文件系统中发现或同步附属元数据", "slideshow_duration_description": "每张图片显示的秒数", - "smart_search_job_description": "对资源运行机器学习以支持智能搜索", - "storage_template_date_time_description": "资源的创建时间戳用于日期时间信息", + "smart_search_job_description": "对资产运行机器学习以支持智能搜索", + "storage_template_date_time_description": "资产的创建时间戳用于日期时间信息", "storage_template_date_time_sample": "示例时间:{date}", "storage_template_enable_description": "启用存储模板引擎", - "storage_template_hash_verification_enabled": "已启用哈希校验", - "storage_template_hash_verification_enabled_description": "开启哈希校验功能。除非你清楚关闭后的后果,否则请勿关闭", + "storage_template_hash_verification_enabled": "启用哈希校验", + "storage_template_hash_verification_enabled_description": "开启哈希校验功能。若不清楚关闭的后果,请勿关闭", "storage_template_migration": "存储模板迁移", - "storage_template_migration_description": "将当前 {template} 应用于已上传的资源", - "storage_template_migration_info": "存储模板会将所有文件扩展名转换为小写。模板更改仅对新上传的资源生效。若要将模板回溯应用于已上传的资源,请运行 {job}。", + "storage_template_migration_description": "将当前 {template} 应用于已上传的资产", + "storage_template_migration_info": "存储模板会将所有文件扩展名转换为小写。模板更改仅对新上传的资产生效。若要将模板回溯应用于已上传的资产,请运行 {job}。", "storage_template_migration_job": "存储模板迁移任务", "storage_template_more_details": "有关此功能的更多详细信息,请参阅 存储模板 及其 含义", "storage_template_onboarding_description_v2": "启用后,此功能将根据用户定义的模板自动整理文件。更多信息,请参阅 文档。", @@ -383,13 +383,13 @@ "transcoding_hardware_acceleration": "硬件加速", "transcoding_hardware_acceleration_description": "实验性功能:速度更快,但在相同码率下质量会降低", "transcoding_hardware_decoding": "硬件解码", - "transcoding_hardware_decoding_setting_description": "启用端到端加速,而不仅仅是加速编码。可能并不适用于所有视频。", + "transcoding_hardware_decoding_setting_description": "启用端到端加速,而不仅仅将加速用于编码。可能并不适用于所有视频。", "transcoding_max_b_frames": "最大B帧数", - "transcoding_max_b_frames_description": "较高的值可以提高压缩效率,但会减慢编码速度。可能与旧设备上的硬件加速不兼容。0表示将禁用B帧,-1表示将自动设置此参数。", + "transcoding_max_b_frames_description": "较高的值可以提高压缩效率,但会减慢编码速度。可能与旧设备上的硬件加速不兼容。0表示禁用B帧,-1表示系统自动设置参数。", "transcoding_max_bitrate": "最高码率", "transcoding_max_bitrate_description": "设置最大比特率可在对输出质量影响较小的情况下,使文件体积更为可控。720p 下,VP9 或 HEVC 普遍将其设为 2600 kbit/s,H.264 则为 4500 kbit/s。如果此项设置为 0,则不限制最大比特率。当没有指定单位时,假设k(代表kbit/s);因此,5000、5000k和5M(Mbit/s)是等效的。", "transcoding_max_keyframe_interval": "最大关键帧间隔", - "transcoding_max_keyframe_interval_description": "设置关键帧之间的最大帧距离。较低的值会降低压缩效率,但可以提高搜索速度,并且可能在快速运动的场景中提高画质。0 表示将自动设置此参数。", + "transcoding_max_keyframe_interval_description": "设置关键帧之间的最大间隔。较低的值会降低压缩效率,但可以加快拖动进度条时的跳转速度,并且可能在快速运动的场景中提高画质。0 表示系统自动设置。", "transcoding_optimal_description": "视频超过目标分辨率或格式不支持", "transcoding_policy": "转码策略", "transcoding_policy_description": "设置视频转码时机", @@ -398,7 +398,7 @@ "transcoding_preset_preset": "预设(-preset)", "transcoding_preset_preset_description": "压缩速度。预设速度越慢,生成的文件越小;在设定特定码率时,还能提升画质。VP9 编码器会忽略(不支持)高于“faster”速度的选项。", "transcoding_reference_frames": "参考帧", - "transcoding_reference_frames_description": "在压缩指定帧时,所参考的帧数量。数值越高,压缩效率越高,但会降低编码速度。设为 0 表示由系统自动设置。", + "transcoding_reference_frames_description": "在压缩指定帧时,所参考的帧数量。数值越高,压缩效率越高,但会降低编码速度。0 表示由系统自动设置。", "transcoding_required_description": "仅非标准格式的视频", "transcoding_settings": "视频转码设置", "transcoding_settings_description": "管理需要转码的视频范围,以及具体的处理方式", @@ -440,9 +440,9 @@ "user_settings_description": "管理用户设置", "user_successfully_removed": "用户 {email} 已成功删除。", "users_page_description": "管理用户页面", - "version_check_enabled_description": "启用版本检测", + "version_check_enabled_description": "检查软件新版本", "version_check_implications": "版本检查功能依赖于与 github.com 的定期通信", - "version_check_settings": "版本检查", + "version_check_settings": "新版本检查", "version_check_settings_description": "启用/禁用新版本通知", "video_conversion_job": "转码视频", "video_conversion_job_description": "对视频进行转码,以兼容更多的浏览器和设备" @@ -478,7 +478,7 @@ "album_added_notification_setting_description": "当您被添加到共享相册时,接收邮箱通知", "album_cover_updated": "封面已更新", "album_delete_confirmation": "确定要删除相册 “{album}” 吗?", - "album_delete_confirmation_description": "如果此相册已被共享,其他用户将无法再访问它。", + "album_delete_confirmation_description": "如果此相册已被共享,其他用户也将无法再访问它。", "album_deleted": "相册已删除", "album_info_card_backup_album_excluded": "已排除", "album_info_card_backup_album_included": "已包含", @@ -510,7 +510,7 @@ "albums": "相册", "albums_count": "{count, plural, one {{count, number} 个相册} other {{count, number} 个相册}}", "albums_default_sort_order": "默认相册排序方式", - "albums_default_sort_order_description": "创建新相册时,初始照片的排序方式。", + "albums_default_sort_order_description": "创建新相册时,影像的初始排序方式。", "albums_feature_description": "可与其他用户共享的照片/内容合集。", "albums_on_device_count": "设备上的相册({count} 个)", "albums_selected": "{count, plural, one {# 个相册已选择} other {# 个相册已选择}}", @@ -537,7 +537,7 @@ "app_bar_signout_dialog_content": "您确定要退出吗?", "app_bar_signout_dialog_ok": "是", "app_bar_signout_dialog_title": "退出登录", - "app_download_links": "APP下载链接", + "app_download_links": "应用下载链接", "app_settings": "应用设置", "app_stores": "应用商店", "app_update_available": "应用更新已发布", @@ -546,7 +546,7 @@ "archive": "归档", "archive_action_prompt": "已将 {count} 项添加到归档", "archive_or_unarchive_photo": "归档或取消归档照片", - "archive_page_no_archived_assets": "未找到已归的资源", + "archive_page_no_archived_assets": "未找到已归档的影像", "archive_page_title": "归档({count})", "archive_size": "归档大小", "archive_size_description": "配置下载的归档大小(GiB)", @@ -675,45 +675,45 @@ "backup_controller_page_total_sub": "包含所选相册内全部唯一的照片和视频", "backup_controller_page_turn_off": "关闭前台备份", "backup_controller_page_turn_on": "开启前台备份", - "backup_controller_page_uploading_file_info": "正在上传中的文件信息", + "backup_controller_page_uploading_file_info": "正在上传文件信息", "backup_err_only_album": "无法删除唯一的相册", "backup_error_sync_failed": "同步失败。无法处理备份。", - "backup_info_card_assets": "项", + "backup_info_card_assets": "资产", "backup_manual_cancelled": "已取消", "backup_manual_in_progress": "上传正在进行中,请稍后再试", "backup_manual_success": "成功", "backup_manual_title": "上传状态", "backup_options": "备份选项", "backup_options_page_title": "备份选项", - "backup_setting_subtitle": "管理后台和前台上传设置", + "backup_setting_subtitle": "管理后台与前台上传设置", "backup_settings_subtitle": "管理上传设置", - "backup_upload_details_page_more_details": "点击了解详情", + "backup_upload_details_page_more_details": "点击查看详情", "backward": "后退", - "biometric_auth_enabled": "生物识别身份验证已启用", - "biometric_locked_out": "您被锁定在生物识别身份验证之外", - "biometric_no_options": "没有可用的生物识别选项", - "biometric_not_available": "生物识别身份验证在此设备上不可用", + "biometric_auth_enabled": "生物识别认证已启用", + "biometric_locked_out": "您已被锁定,无法使用生物识别认证", + "biometric_no_options": "无可用的生物识别选项", + "biometric_not_available": "本设备不支持生物识别认证", "birthdate_saved": "出生日期保存成功", - "birthdate_set_description": "出生日期用于计算照片中该人物在拍照时的年龄。", - "blurred_background": "背景模糊", - "bugs_and_feature_requests": "Bug 与功能请求", + "birthdate_set_description": "出生日期用于计算拍摄此照片时此人的年龄。", + "blurred_background": "背景虚化", + "bugs_and_feature_requests": "问题与功能建议", "build": "构建版本", "build_image": "镜像版本", - "bulk_delete_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复资产} other {#个重复资产}}吗?这将保留每个组中最大的项目并永久删除所有其它重复资产。注意:该操作无法被撤消!", - "bulk_keep_duplicates_confirmation": "您确定要保留{count, plural, one {#个重复资产} other {#个重复资产}}吗?这将清空所有重复记录,但不会删除任何内容。", - "bulk_trash_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复资产} other {#个重复资产}}吗?这将保留每组中最大的资产并删除所有其它重复资产。", + "bulk_delete_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项} other {#个重复项}}吗?这将保留每组中体积最大的文件,并永久删除其余所有重复项。该操作无法被撤消!", + "bulk_keep_duplicates_confirmation": "您确定要保留{count, plural, one {#个重复项} other {#个重复项}}吗?这将标记所有重复组为已解决,且不会删除任何文件。", + "bulk_trash_duplicates_confirmation": "您确定要批量将{count, plural, one {#个重复项} other {#个重复项}}移至回收站吗?这将保留每组中体积最大的文件,并将其余所有重复项移至回收站。", "buy": "购买 Immich", "cache_settings_clear_cache_button": "清除缓存", - "cache_settings_clear_cache_button_title": "清除应用缓存。在重新生成缓存之前,将显著影响应用的性能。", + "cache_settings_clear_cache_button_title": "清理应用缓存。在缓存重建期间,应用的运行速度会明显变慢。", "cache_settings_duplicated_assets_clear_button": "清除", - "cache_settings_duplicated_assets_subtitle": "应用程序忽略的照片和视频", + "cache_settings_duplicated_assets_subtitle": "忽略列表中的媒体文件", "cache_settings_duplicated_assets_title": "重复资产({count})", - "cache_settings_statistics_album": "资产库缩略图", - "cache_settings_statistics_full": "完整图像", + "cache_settings_statistics_album": "图库缩略图", + "cache_settings_statistics_full": "原图", "cache_settings_statistics_shared": "共享相册缩略图", "cache_settings_statistics_thumbnail": "缩略图", - "cache_settings_statistics_title": "缓存使用情况", - "cache_settings_subtitle": "控制 Immich app 的缓存行为", + "cache_settings_statistics_title": "缓存占用情况", + "cache_settings_subtitle": "管理 Immich 手机端的缓存", "cache_settings_tile_subtitle": "设置本地存储行为", "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", @@ -782,6 +782,8 @@ "client_cert_import": "导入", "client_cert_import_success_msg": "客户端证书已导入", "client_cert_invalid_msg": "无效的证书文件或密码错误", + "client_cert_password_message": "输入证书的密码", + "client_cert_password_title": "证书密码", "client_cert_remove_msg": "客户端证书已移除", "client_cert_subtitle": "仅支持PKCS12(.p12、.pfx)格式。证书导入/删除仅在登录前可用", "client_cert_title": "SSL 客户端证书[实验性]", @@ -995,6 +997,11 @@ "editor_close_without_save_prompt": "此更改不会被保存", "editor_close_without_save_title": "关闭编辑器?", "editor_confirm_reset_all_changes": "您确定要重置所有更改吗?", + "editor_discard_edits_confirm": "放弃编辑", + "editor_discard_edits_prompt": "您有未保存的更改,确定要放弃吗?", + "editor_discard_edits_title": "确定放弃编辑吗?", + "editor_edits_applied_error": "应用编辑失败", + "editor_edits_applied_success": "编辑已成功应用", "editor_flip_horizontal": "水平翻转", "editor_flip_vertical": "垂直翻转", "editor_orientation": "方向", @@ -1196,10 +1203,12 @@ "features_in_development": "开发中的功能", "features_setting_description": "管理 App 功能", "file_name_or_extension": "文件名或扩展名", + "file_name_text": "文件名", + "file_name_with_value": "文件名:{file_name}", "file_size": "大小", "filename": "文件名", "filetype": "文件类型", - "filter": "筛选器", + "filter": "滤镜", "filter_description": "目标项目筛选条件", "filter_people": "筛选人物", "filter_places": "筛选地点", @@ -1604,7 +1613,6 @@ "not_available": "不适用", "not_in_any_album": "不在任何相册中", "not_selected": "未选择", - "note_apply_storage_label_to_previously_uploaded assets": "提示:要将存储标签应用于之前上传的项目,请运行此", "notes": "提示", "nothing_here_yet": "这里什么都没有", "notification_permission_dialog_content": "要启用通知,请转到“设置”,并选择“允许”。", @@ -1806,7 +1814,7 @@ "reassigned_assets_to_new_person": "重新指派{count, plural, one {#个项目} other {#个项目}}到新的人物", "reassing_hint": "指派选择的项目到已存在的人物", "recent": "最近", - "recent-albums": "最近的相册", + "recent_albums": "最近的相册", "recent_searches": "最近搜索", "recently_added": "近期添加", "recently_added_page_title": "最近添加", @@ -1819,7 +1827,7 @@ "refresh_thumbnails": "刷新缩略图", "refreshed": "已刷新", "refreshes_every_file": "重新扫描所有现有文件和新文件", - "refreshing_encoded_video": "正在刷新已编码视频", + "refreshing_encoded_video": "刷新已编码视频", "refreshing_faces": "刷新面部识别", "refreshing_metadata": "刷新元数据", "regenerating_thumbnails": "重新生成缩略图", diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index 3742ca34f8..60dae6ed22 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -3,134 +3,134 @@ "account": "帳號", "account_settings": "帳號設定", "acknowledge": "了解", - "action": "操作", + "action": "動作", "action_common_update": "更新", - "action_description": "對篩選後的資產執行的一組操作", - "actions": "進行動作", + "action_description": "對篩選後的項目執行一組動作", + "actions": "動作", "active": "處理中", "active_count": "處理中:{count}", "activity": "動態", - "activity_changed": "動態已{enabled, select, true {開啟} other {關閉}}", + "activity_changed": "動態已{enabled, select, true {啟用} other {停用}}", "add": "加入", - "add_a_description": "新增描述", + "add_a_description": "新增說明", "add_a_location": "新增地點", - "add_a_name": "加入姓名", + "add_a_name": "新增姓名", "add_a_title": "新增標題", - "add_action": "添加動作", - "add_action_description": "按一下以添加要執行的操作", - "add_assets": "添加資源", + "add_action": "新增動作", + "add_action_description": "按一下以新增要執行的動作", + "add_assets": "新增項目", "add_birthday": "新增生日", "add_endpoint": "新增端點", - "add_exclusion_pattern": "加入篩選條件", - "add_filter": "添加篩選器", - "add_filter_description": "按一下以添加篩選條件", + "add_exclusion_pattern": "新增排除模式", + "add_filter": "新增篩選器", + "add_filter_description": "按一下以新增篩選條件", "add_location": "新增地點", "add_more_users": "新增其他使用者", - "add_partner": "新增親朋好友", + "add_partner": "新增親友", "add_path": "新增路徑", - "add_photos": "加入照片", + "add_photos": "加入相片", "add_tag": "加入標籤", - "add_to": "加入到…", + "add_to": "加入至…", "add_to_album": "加入到相簿", - "add_to_album_bottom_sheet_added": "新增到 {album}", + "add_to_album_bottom_sheet_added": "已新增至 {album}", "add_to_album_bottom_sheet_already_exists": "已在 {album} 中", - "add_to_album_bottom_sheet_some_local_assets": "無法將某些本機資產新增到相簿", - "add_to_album_toggle": "選擇相簿{album}", + "add_to_album_bottom_sheet_some_local_assets": "無法將部分本機項目新增至相簿", + "add_to_album_toggle": "選取相簿 {album}", "add_to_albums": "加入相簿", "add_to_albums_count": "將 ({count}) 個項目加入相簿", "add_to_bottom_bar": "新增到", - "add_to_shared_album": "加到共享相簿", + "add_to_shared_album": "新增至共享相簿", "add_upload_to_stack": "新增上傳到堆疊", "add_url": "新增 URL", - "add_workflow_step": "添加工作流步驟", + "add_workflow_step": "新增工作流程步驟", "added_to_archive": "移至封存", "added_to_favorites": "加入收藏", - "added_to_favorites_count": "將 {count, number} 個項目加入收藏", + "added_to_favorites_count": "已將 {count, number} 個項目加入收藏", "admin": { - "add_exclusion_pattern_description": "新增排除條件。支援使用「*」、「 **」、「?」來找尋符合規則的字串。如果要在任何名為「Raw」的目錄內排除所有符合條件的檔案,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。", + "add_exclusion_pattern_description": "新增排除模式。支援使用 *、** 與 ? 進行萬用字元比對 (Globbing)。若要忽略任何名為「Raw」目錄中的所有檔案,請使用「**/Raw/**」;若要忽略所有以「.tif」結尾的檔案,請使用「**/*.tif」;若要忽略特定的絕對路徑,請使用「/path/to/ignore/**」。", "admin_user": "管理員", - "asset_offline_description": "此外部媒體庫項目已無法在磁碟上找到,並已移至垃圾桶。若該檔案是在媒體庫內移動,請在時間軸中檢視新的對應項目。若要還原此項目,請確保下方的檔案路徑可供 Immich 存取,並重新掃描媒體庫。", + "asset_offline_description": "此外部媒體庫項目已無法在磁碟上找到,並已移至垃圾桶。若該檔案是在媒體庫內移動,請在時間軸中查看新的對應項目。若要還原此項目,請確保下方的檔案路徑可供 Immich 存取,並重新掃描媒體庫。", "authentication_settings": "驗證設定", "authentication_settings_description": "管理密碼、OAuth 與其他驗證設定", - "authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入。", - "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令 。", + "authentication_settings_disable_all": "您確定要停用所有登入方式嗎?這將導致完全無法登入。", + "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令。", "background_task_job": "背景工作", "backup_database": "建立資料庫備份", "backup_database_enable_description": "啟用資料庫備份", "backup_keep_last_amount": "保留先前備份的數量", "backup_onboarding_1_description": "在雲端或其他實體位置的異地備份副本。", - "backup_onboarding_2_description": "儲存在不同裝置上的本機副本。這包括主要檔案及其本機備份。", + "backup_onboarding_2_description": "儲存在不同裝置上的本機副本。這包含主要檔案及其本機備份。", "backup_onboarding_3_description": "您資料的總備份份數,包括原始檔案在內。這包括 1 份異地備份與 2 份本機副本。", - "backup_onboarding_description": "建議採用 3-2-1 備份策略 來保護您的資料。您應保留已上傳的照片/影片副本,以及 Immich 資料庫,以建立完整的備份方案。", + "backup_onboarding_description": "建議採用 3-2-1 備份策略 來保護您的資料。您應保留已上傳的相片/影片副本,以及 Immich 資料庫,以建立完整的備份方案。", "backup_onboarding_footer": "更多備份 Immich 資訊,請參考說明文件。", "backup_onboarding_parts_title": "遵從備份原則 3-2-1:", "backup_onboarding_title": "備份", "backup_settings": "資料庫備份設定", "backup_settings_description": "管理資料庫備份設定。", "cleared_jobs": "已刪除「{job}」任務", - "config_set_by_file": "目前的設定是由設定檔設定", + "config_set_by_file": "目前的設定是由設定檔所設定", "confirm_delete_library": "您確定要刪除外部媒體庫 {library} 嗎?", - "confirm_delete_library_assets": "您確定要刪除此外部媒體庫嗎?這將從 Immich 中刪除 {count, plural, one {# 個項目} other {# 個項目}} ,且無法復原。檔案仍會保留在硬碟中。", + "confirm_delete_library_assets": "您確定要刪除此媒體庫嗎?這將從 Immich 中刪除 {count, plural, one {# 個項目} other {所有 # 個項目}}且無法復原。檔案仍會保留在磁碟中。", "confirm_email_below": "請在底下輸入 {email} 以確認", "confirm_reprocess_all_faces": "您確定要重新處理所有臉孔嗎?這會清除已命名的人物。", "confirm_user_password_reset": "您確定要重設 {user} 的密碼嗎?", "confirm_user_pin_code_reset": "確定要重設 {user} 的 PIN 碼嗎?", - "copy_config_to_clipboard_description": "將當前系統配寘作為JSON對象複製到剪貼板", + "copy_config_to_clipboard_description": "將目前系統設定以 JSON 物件格式複製到剪貼簿", "create_job": "建立任務", "cron_expression": "Cron 表達式", "cron_expression_description": "使用 Cron 格式設定掃描間隔。更多資訊請參閱 Crontab Guru", - "cron_expression_presets": "Cron 表達式預設範本", + "cron_expression_presets": "Cron 表達式預設值", "disable_login": "停用登入", "duplicate_detection_job_description": "依靠智慧搜尋。對項目執行機器學習來偵測相似圖片", - "exclusion_pattern_description": "排除規則可讓您在掃描媒體庫時忽略特定的檔案和資料夾。這在您有些資料夾包含不想匯入的檔案(例如 RAW 檔)時特別有用。", - "export_config_as_json_description": "將當前系統配寘下載為JSON檔案", - "external_libraries_page_description": "管理外部庫頁面", + "exclusion_pattern_description": "排除模式可讓您在掃描媒體庫時忽略特定檔案與資料夾。若某些資料夾包含您不想匯入的檔案(例如 RAW 檔),此功能將非常有用。", + "export_config_as_json_description": "將目前系統設定下載為 JSON 檔案", + "external_libraries_page_description": "管理外部媒體庫頁面", "face_detection": "臉孔偵測", - "face_detection_description": "使用機器學習偵測媒體檔案中的臉孔。對於影片,僅會分析縮圖。「重新整理」會(重新)處理所有媒體檔案。「重設」則會額外清除目前的所有人臉資料。「排入未處理」會將尚未處理過的媒體檔案加入佇列。在完成「臉孔偵測』後,偵測到的臉孔將會被加入「臉孔辨識」的佇列,並依照辨識結果歸類到現有或新的人物群組中。", - "facial_recognition_job_description": "將偵測到的臉孔依照人物分類。此步驟會在臉孔偵測完成後執行。選擇「重設」會重新分組所有臉孔。選擇「排入未處理」會將尚未指派人物的臉孔加入佇列。", + "face_detection_description": "使用機器學習偵測項目中的臉孔。對於影片,僅會分析縮圖。「重新整理」會(重新)處理所有項目;「重設」則會額外清除目前的臉孔資料;「排入未處理」會將尚未處理的項目加入佇列。完成「臉孔偵測」後,偵測到的臉孔將加入「臉孔辨識」佇列,並歸類至現有或新的人物群組。", + "facial_recognition_job_description": "將偵測到的臉孔歸類為人物。此步驟會在臉孔偵測完成後執行。「重設」會重新對所有臉孔進行分群;「排入未處理」則會將尚未指派人物的臉孔加入佇列。", "failed_job_command": "{job} 任務的 {command} 指令執行失敗", - "force_delete_user_warning": "警告:這將立即刪除使用者及其所有項目。此操作無法撤銷並且無法還原刪除的檔案。", + "force_delete_user_warning": "警告:這將立即刪除使用者及其所有項目。此動作無法復原,且無法找回已刪除的檔案。", "image_format": "格式", "image_format_description": "WebP 能產生相對於 JPEG 更小的檔案,但編碼速度較慢。", "image_fullsize_description": "移除中繼資料的大尺寸影像,在放大圖片時使用", "image_fullsize_enabled": "啟用大尺寸影像產生", - "image_fullsize_enabled_description": "產生非網頁友善格式的大尺寸影像。啟用「偏好嵌入的預覽」時,會直接使用內嵌預覽而不進行轉換。不會影響 JPEG 等網頁友善格式。", + "image_fullsize_enabled_description": "為非網頁友善格式產生大尺寸相片。啟用「偏好內嵌預覽」時,系統將直接使用內嵌預覽而不進行轉碼,不影響 JPEG 等網頁友善格式。", "image_fullsize_quality_description": "大尺寸影像品質,範圍為 1 到 100。數值越高品質越好,但檔案也會越大。", "image_fullsize_title": "大尺寸影像設定", - "image_prefer_embedded_preview": "偏好嵌入的預覽", - "image_prefer_embedded_preview_setting_description": "在可行的情況下,將 RAW 照片中的內嵌預覽用作影像處理的輸入來源。這對某些影像能產生更準確的色彩,但預覽的品質取決於相機,影像可能會出現更多壓縮瑕疵。", + "image_prefer_embedded_preview": "偏好內嵌預覽", + "image_prefer_embedded_preview_setting_description": "在可用時,將 RAW 相片中的內嵌預覽作為影像處理的輸入來源。雖然這能讓部分相片色彩更準確,但預覽品質取決於相機,且影像可能會出現較多壓縮瑕疵。", "image_prefer_wide_gamut": "偏好廣色域", - "image_prefer_wide_gamut_setting_description": "使用 Display P3 來製作縮圖。這能更好地保留寬廣色域影像的鮮豔度,但在舊裝置與舊版本瀏覽器上,影像可能會呈現不同的效果。sRGB 影像會保持為 sRGB,以避免色彩偏移。", - "image_preview_description": "移除中繼資料的中尺寸影像,用於檢視單一媒體檔案以及機器學習時使用", + "image_prefer_wide_gamut_setting_description": "使用 Display P3 製作縮圖。這能更好地保留廣色域影像的鮮豔度,但在舊裝置與舊版瀏覽器上,影像呈現的效果可能會有所不同。sRGB 影像將保持為 sRGB,以避免色彩偏移。", + "image_preview_description": "中等尺寸影像(不含中繼資料),用於檢視單一項目與機器學習", "image_preview_quality_description": "預覽品質範圍為 1 到 100。數值越高品質越好,但檔案也會更大,並可能降低應用程式的回應速度。設定過低的數值可能會影響機器學習的品質。", "image_preview_title": "預覽設定", "image_progressive": "逐步", - "image_progressive_description": "對JPEG圖像進行逐步編碼,以實現漸進式加載顯示。這不會影響WebP圖像。", + "image_progressive_description": "對 JPEG 影像進行漸進式編碼,以實現漸進式載入顯示。這不會影響 WebP 影像。", "image_quality": "品質", "image_resolution": "解析度", "image_resolution_description": "較高的解析度能保留更多細節,但編碼時間會更長、檔案大小會更大,並可能降低應用程式的回應速度。", "image_settings": "圖片設定", - "image_settings_description": "管理產生圖片的品質和解析度", - "image_thumbnail_description": "移除中繼資料的小型縮圖,以用於檢視大量照片時使用,例如主時間軸", + "image_settings_description": "管理產生的影像品質與解析度", + "image_thumbnail_description": "移除中繼資料的小型縮圖,以用於檢視大量相片時使用,例如主時間軸", "image_thumbnail_quality_description": "縮圖品質範圍為 1 到 100。數值越高品質越好,但檔案也會更大,並可能降低應用程式的回應速度。", "image_thumbnail_title": "縮圖設定", - "import_config_from_json_description": "通過上傳JSON設定檔導入系統配寘", - "job_concurrency": "{job}併發", + "import_config_from_json_description": "透過上傳 JSON 設定檔匯入系統設定", + "job_concurrency": "{job} 並行數", "job_created": "已建立任務", - "job_not_concurrency_safe": "這個任務併發並不安全。", + "job_not_concurrency_safe": "此任務不支援並行執行。", "job_settings": "任務設定", - "job_settings_description": "併發任務管理", + "job_settings_description": "管理任務並行數", "jobs_delayed": "{jobCount, plural, other {# 項任務已延後}}", "jobs_failed": "{jobCount, plural, other {# 項任務已失敗}}", - "jobs_over_time": "組織時間任務數", + "jobs_over_time": "任務數量趨勢", "library_created": "已建立媒體庫:{library}", "library_deleted": "媒體庫已刪除", - "library_details": "媒體庫詳情", - "library_folder_description": "指定要導入的資料夾。 將掃描此資料夾(包括子資料夾)中的影像和視頻。", - "library_remove_exclusion_pattern_prompt": "您確定要删除此排除模式嗎?", - "library_remove_folder_prompt": "您確定要删除此導入資料夾嗎?", + "library_details": "媒體庫詳細資訊", + "library_folder_description": "指定要匯入的資料夾。系統將掃描此資料夾(包含子資料夾)中的影像與影片。", + "library_remove_exclusion_pattern_prompt": "確定要移除此排除模式嗎?", + "library_remove_folder_prompt": "確定要移除此匯入資料夾嗎?", "library_scanning": "定期掃描", - "library_scanning_description": "定期媒體庫掃描設定", + "library_scanning_description": "設定定期媒體庫掃描", "library_scanning_enable_description": "啟用媒體庫定期掃描", "library_settings": "外部媒體庫", "library_settings_description": "管理外部媒體庫設定", @@ -139,9 +139,9 @@ "library_watching_enable_description": "監控外部媒體庫的檔案變化", "library_watching_settings": "媒體庫監控[實驗性]", "library_watching_settings_description": "自動監控檔案的變化", - "logging_enable_description": "啟用日誌記錄", - "logging_level_description": "啟用時的日誌層級。", - "logging_settings": "日誌", + "logging_enable_description": "啟用紀錄功能", + "logging_level_description": "啟用時的紀錄層級。", + "logging_settings": "紀錄", "machine_learning_availability_checks": "可用性檢查", "machine_learning_availability_checks_description": "自動偵測並優先選擇可用的機器學習伺服器", "machine_learning_availability_checks_enabled": "啟用可用性檢查", @@ -150,64 +150,64 @@ "machine_learning_availability_checks_timeout": "請求超時", "machine_learning_availability_checks_timeout_description": "可用性檢查超時(毫秒)", "machine_learning_clip_model": "CLIP 模型", - "machine_learning_clip_model_description": "這裡有份 CLIP 模型名單。注意:更換模型後須對所有圖片重新執行「智慧搜尋」任務。", + "machine_learning_clip_model_description": "這裡有份 CLIP 模型清單。注意:更換模型後必須對所有相片重新執行「智慧搜尋」任務。", "machine_learning_duplicate_detection": "重複項目偵測", "machine_learning_duplicate_detection_enabled": "啟用重複項目偵測", - "machine_learning_duplicate_detection_enabled_description": "若停用,完全相同的媒體檔案仍會進行重複資料刪除。", + "machine_learning_duplicate_detection_enabled_description": "若停用,完全相同的項目仍會進行重複項目刪除。", "machine_learning_duplicate_detection_setting_description": "使用 CLIP 向量比對尋找可能的重複項目", "machine_learning_enabled": "啟用機器學習", - "machine_learning_enabled_description": "若停用,則無視下方的設定,所有機器學習的功能都將停用。", + "machine_learning_enabled_description": "若停用,不論下方的設定為何,所有機器學習功能都將停用。", "machine_learning_facial_recognition": "人臉辨識", "machine_learning_facial_recognition_description": "偵測、辨識並對圖片中的臉孔分類", "machine_learning_facial_recognition_model": "人臉辨識模型", - "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。大的模型較慢且使用較多記憶體,但成效較佳。更換模型後需對所有影像重新執行「人臉辨識」。", + "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。較大的模型速度較慢且佔用較多記憶體,但效果較佳。請注意,更換模型後必須對所有影像重新執行「臉孔偵測」任務。", "machine_learning_facial_recognition_setting": "啟用人臉辨識", - "machine_learning_facial_recognition_setting_description": "若停用,影像將不會產生人臉辨識編碼,並且在「探索」頁面不會有「人物」功能。", + "machine_learning_facial_recognition_setting_description": "若停用,影像將不會進行人臉辨識編碼,且「探索」頁面的「人物」區塊將不會顯示任何內容。", "machine_learning_max_detection_distance": "偵測距離上限", "machine_learning_max_detection_distance_description": "若兩張影像間的距離小於此將被判斷為相同,範圍為 0.001-0.1。數值越高能偵測到越多重複,但也更有可能誤判。", "machine_learning_max_recognition_distance": "辨識距離上限", - "machine_learning_max_recognition_distance_description": "兩張臉孔被視為同一人物的最大距離,範圍為 0 至 2。降低此數值可避免將不同人物標記為同一人;提高此數值則可避免將同一人物標記為兩個不同的人。請注意,合併兩個人物比將一個人物拆分成兩個更容易,因此在可能的情況下,建議將此閾值設定得較低。", + "machine_learning_max_recognition_distance_description": "兩張臉孔被視為同一人物的最大距離,範圍為 0 至 2。降低此數值可避免將不同人物標記為同一人;提高此數值則可避免將同一人物標記為兩個不同的人。請注意,合併人物比拆分人物更容易,因此建議在可能的情況下將此門檻值設定得較低。", "machine_learning_min_detection_score": "最低偵測分數", - "machine_learning_min_detection_score_description": "臉孔偵測的最低信心分數,範圍為 0 至 1。數值較低時會偵測到更多臉孔,但可能導致誤判。", + "machine_learning_min_detection_score_description": "臉孔偵測的最低信心分數,範圍為 0 - 1。較低的數值會偵測到更多臉孔,但可能導致誤判。", "machine_learning_min_recognized_faces": "最低臉部辨識數量", "machine_learning_min_recognized_faces_description": "建立新人物所需的最低已辨識臉孔數量。提高此數值可讓臉孔辨識更精確,但同時會增加臉孔未被指派給任何人物的可能性。", "machine_learning_ocr": "文字辨識(OCR)", - "machine_learning_ocr_description": "使用機器學習來識別圖片中的文字", + "machine_learning_ocr_description": "使用機器學習辨識影像中的文字", "machine_learning_ocr_enabled": "啟用OCR", - "machine_learning_ocr_enabled_description": "如果禁用,影像將不會進行文字識別。", - "machine_learning_ocr_max_resolution": "最大分辯率", - "machine_learning_ocr_max_resolution_description": "高於此分辯率的預覽將調整大小,同時保持縱橫比。 更高的值更準確,但處理時間更長,佔用更多記憶體。", + "machine_learning_ocr_enabled_description": "若停用,影像將不會進行文字辨識。", + "machine_learning_ocr_max_resolution": "最大解析度", + "machine_learning_ocr_max_resolution_description": "解析度高於此值的預覽影像將在保持長寬比的情況下調整大小。數值越高越準確,但處理時間更長且會佔用更多記憶體。", "machine_learning_ocr_min_detection_score": "最低檢測分數", - "machine_learning_ocr_min_detection_score_description": "要檢測的文字的最小置信度分數為0-1。 較低的值將檢測到更多的文字,但可能會導致誤報。", - "machine_learning_ocr_min_recognition_score": "最低識別分數", - "machine_learning_ocr_min_score_recognition_description": "檢測到的文字的最小置信度得分為0-1。 較低的值將識別更多的文字,但可能會導致誤報。", + "machine_learning_ocr_min_detection_score_description": "文字偵測的最低信心分數,範圍為 0 - 1。較低的數值會偵測到更多文字,但可能導致誤判。", + "machine_learning_ocr_min_recognition_score": "最低辨識分數", + "machine_learning_ocr_min_score_recognition_description": "已偵測文字的最低辨識信心分數,範圍為 0 - 1。較低的數值會辨識出更多文字,但可能導致誤判。", "machine_learning_ocr_model": "OCR模型", - "machine_learning_ocr_model_description": "服務器模型比移動模型更準確,但需要更長的時間來處理和使用更多的記憶體。", + "machine_learning_ocr_model_description": "伺服器模型比行動裝置模型更準確,但處理時間較長且會佔用更多記憶體。", "machine_learning_settings": "機器學習設定", "machine_learning_settings_description": "管理機器學習的功能和設定", "machine_learning_smart_search": "智慧搜尋", "machine_learning_smart_search_description": "使用 CLIP 嵌入向量以語意方式搜尋影像", "machine_learning_smart_search_enabled": "啟用智慧搜尋", - "machine_learning_smart_search_enabled_description": "如果停用,影像將不會被編碼以進行智慧搜尋。", + "machine_learning_smart_search_enabled_description": "若停用,影像將不會進行智慧搜尋編碼。", "machine_learning_url_description": "機器學習伺服器的 URL。若提供多個 URL,系統會依序逐一嘗試,直到其中一臺成功回應為止(由前到後)。未回應的伺服器將被暫時忽略,直到其重新上線。", "maintenance_delete_backup": "刪除備份", - "maintenance_delete_backup_description": "此文件將被永久刪除。", + "maintenance_delete_backup_description": "此檔案將被永久刪除且無法復原。", "maintenance_delete_error": "刪除備份失敗。", - "maintenance_restore_backup": "恢復備份", - "maintenance_restore_backup_description": "Immich數據將被請出,并從選定的備份中恢復。在繼續之前,將先創建一個當前數據的備份。", - "maintenance_restore_backup_different_version": "此備份是由不同版本的Immich創建的!", + "maintenance_restore_backup": "還原備份", + "maintenance_restore_backup_description": "Immich 的資料將被清除,並從選取的備份還原。在繼續操作前,系統會先建立目前的資料備份。", + "maintenance_restore_backup_different_version": "此備份是由不同版本的 Immich 所建立!", "maintenance_restore_backup_unknown_version": "無法確定備份版本。", - "maintenance_restore_database_backup": "恢復數據庫備份", - "maintenance_restore_database_backup_description": "使用備份文件將數據庫回滾到較早的狀態", + "maintenance_restore_database_backup": "還原資料庫備份", + "maintenance_restore_database_backup_description": "使用備份檔案將資料庫還原至較早的狀態", "maintenance_settings": "維護", - "maintenance_settings_description": "將Immich置於維護模式。", + "maintenance_settings_description": "將 Immich 切換至維護模式。", "maintenance_start": "啟動維護模式", "maintenance_start_error": "啟動維護模式失敗。", - "maintenance_upload_backup": "上傳數據庫備份文件", - "maintenance_upload_backup_error": "無法上傳備份,它是.sql或.sql.gz格式的文件嗎?", - "manage_concurrency": "管理併發", - "manage_concurrency_description": "導航到任務頁面以管理任務併發性", - "manage_log_settings": "管理日誌設定", + "maintenance_upload_backup": "上傳資料庫備份檔案", + "maintenance_upload_backup_error": "無法上傳備份,它是 .sql 或 .sql.gz 格式的檔案嗎?", + "manage_concurrency": "管理並行設定", + "manage_concurrency_description": "前往任務頁面以管理任務並行設定", + "manage_log_settings": "管理紀錄設定", "map_dark_style": "深色樣式", "map_enable_description": "啟用地圖功能", "map_gps_settings": "地圖與 GPS 設定", @@ -224,21 +224,21 @@ "memory_cleanup_job": "回憶清理", "memory_generate_job": "產生回憶", "metadata_extraction_job": "擷取中繼資料", - "metadata_extraction_job_description": "從每個媒體檔案中擷取中繼資料資訊,例如 GPS、臉孔與解析度", + "metadata_extraction_job_description": "從每個項目中擷取中繼資料資訊,例如 GPS、臉孔與解析度", "metadata_faces_import_setting": "啟用臉孔匯入", - "metadata_faces_import_setting_description": "從影像 EXIF 資料與側接檔案匯入臉孔", + "metadata_faces_import_setting_description": "從影像 EXIF 資料與 Sidecar 檔案匯入臉孔", "metadata_settings": "中繼資料設定", "metadata_settings_description": "管理中繼資料設定", "migration_job": "遷移", - "migration_job_description": "將媒體檔案與臉孔的縮圖遷移至最新的資料夾結構", + "migration_job_description": "將項目與臉孔縮圖遷移至最新的資料夾結構", "nightly_tasks_cluster_faces_setting_description": "對新偵測到的臉孔執行臉孔辨識", "nightly_tasks_cluster_new_faces_setting": "為新臉孔進行分群", "nightly_tasks_database_cleanup_setting": "資料庫清理作業", "nightly_tasks_database_cleanup_setting_description": "清除資料庫中舊的與已過期的資料", "nightly_tasks_generate_memories_setting": "產生回憶", - "nightly_tasks_generate_memories_setting_description": "從媒體檔案建立新回憶", + "nightly_tasks_generate_memories_setting_description": "從項目建立新回憶", "nightly_tasks_missing_thumbnails_setting": "產生缺少的縮圖", - "nightly_tasks_missing_thumbnails_setting_description": "將沒有縮圖的媒體檔案排入佇列以產生縮圖", + "nightly_tasks_missing_thumbnails_setting_description": "將缺少縮圖的項目排入佇列以產生縮圖", "nightly_tasks_settings": "夜間任務設定", "nightly_tasks_settings_description": "管理夜間任務", "nightly_tasks_start_time_setting": "開始時間", @@ -247,7 +247,7 @@ "nightly_tasks_sync_quota_usage_setting_description": "根據目前的使用量更新使用者的儲存配額", "no_paths_added": "沒有已新增的路徑", "no_pattern_added": "尚未新增排除規則", - "note_apply_storage_label_previous_assets": "提示:若要將儲存標籤套用到先前上傳的媒體檔案,請執行", + "note_apply_storage_label_previous_assets": "提示:若要將儲存標籤套用至先前上傳的項目,請執行", "note_cannot_be_changed_later": "注意:此設定日後無法變更!", "notification_email_from_address": "寄件地址", "notification_email_from_address_description": "寄件者電子郵件地址,例如:\"Immich Photo Server \"。請確保使用的是您有權限寄送郵件的地址。", @@ -255,7 +255,7 @@ "notification_email_ignore_certificate_errors": "忽略憑證錯誤", "notification_email_ignore_certificate_errors_description": "忽略 TLS 憑證驗證錯誤(不建議)", "notification_email_password_description": "用於與電子郵件伺服器驗證的密碼", - "notification_email_port_description": "電子郵件伺服器埠口(例如 25、465 或 587)", + "notification_email_port_description": "電子郵件伺服器的連接埠(例如 25、465 或 587)", "notification_email_secure": "SMTPS", "notification_email_secure_description": "使用SMTPS(基於TLS的SMTP)", "notification_email_sent_test_email_button": "傳送測試電子郵件並儲存", @@ -272,7 +272,7 @@ "oauth_auto_register": "自動註冊", "oauth_auto_register_description": "使用 OAuth 登入後自動註冊新使用者", "oauth_button_text": "按鈕文字", - "oauth_client_secret_description": "如果 OAuth 提供者不支援 PKCE(授權碼驗證碼交換機制),則此為必填項目", + "oauth_client_secret_description": "機密用戶端的必填項目;若公開用戶端不支援 PKCE (代碼交換的驗證金鑰),亦須填寫。", "oauth_enable_description": "使用 OAuth 登入", "oauth_mobile_redirect_uri": "行動端重新導向 URI", "oauth_mobile_redirect_uri_override": "覆蓋行動端重新導向 URI", @@ -290,7 +290,7 @@ "oauth_storage_quota_default_description": "未提供宣告時所使用的配額(GiB)。", "oauth_timeout": "請求逾時", "oauth_timeout_description": "請求的逾時時間(毫秒)", - "ocr_job_description": "使用機器學習來識別圖片中的文字", + "ocr_job_description": "使用機器學習辨識影像中的文字", "password_enable_description": "使用電子郵件和密碼登入", "password_settings": "密碼登入", "password_settings_description": "管理密碼登入設定", @@ -311,38 +311,38 @@ "search_jobs": "搜尋任務…", "send_welcome_email": "傳送歡迎電子郵件", "server_external_domain_settings": "外部網域", - "server_external_domain_settings_description": "公開分享連結的網域,包含 http(s)://", + "server_external_domain_settings_description": "公開分享連結的網域", "server_public_users": "公開使用者", - "server_public_users_description": "在將使用者新增到共享相簿時,會列出所有使用者的姓名與電子郵件。停用此功能後,使用者清單將僅供系統管理員檢視。", + "server_public_users_description": "將使用者新增至共享相簿時,會列出所有使用者(姓名與電子郵件)。若停用,使用者清單將僅供管理員查看。", "server_settings": "伺服器設定", "server_settings_description": "管理伺服器設定", - "server_stats_page_description": "管理服務器統計頁面", + "server_stats_page_description": "管理伺服器統計頁面", "server_welcome_message": "歡迎訊息", "server_welcome_message_description": "在登入頁面顯示的訊息。", "settings_page_description": "管理設定頁面", "sidecar_job": "側接檔案中繼資料", "sidecar_job_description": "從檔案系統偵測或同步側接檔案中繼資料", "slideshow_duration_description": "每張圖片放映的秒數", - "smart_search_job_description": "執行機器學習有助於智慧搜尋", + "smart_search_job_description": "對項目執行機器學習以支援智慧搜尋", "storage_template_date_time_description": "檔案的建立時間戳會用於日期與時間資訊", "storage_template_date_time_sample": "取樣時間 {date}", "storage_template_enable_description": "啟用儲存範本引擎", "storage_template_hash_verification_enabled": "雜湊函式驗證已啟用", "storage_template_hash_verification_enabled_description": "啟用雜湊函式驗證,除非您很清楚地知道這個選項的作用,否則請勿停用此功能", "storage_template_migration": "儲存範本遷移", - "storage_template_migration_description": "將目前的 {template} 套用到先前上傳的項目", - "storage_template_migration_info": "儲存範本會將所有副檔名轉換為小寫。範本變更只會套用到新的項目。若要將範本追溯套用到先前上傳的項目,請執行 {job}。", - "storage_template_migration_job": "儲存範本遷移任務", + "storage_template_migration_description": "套用目前的 {template} 至先前上傳的項目", + "storage_template_migration_info": "儲存範本會將所有副檔名轉換為小寫。範本變更僅會套用至新項目。若要追溯套用範本至先前上傳的項目,請執行 {job}。", + "storage_template_migration_job": "儲存範本遷移作業", "storage_template_more_details": "關於此功能的更多詳細資訊,請參閱儲存範本及其影響", - "storage_template_onboarding_description_v2": "啟用後,此功能會依使用者自訂的範本自動整理檔案。更多資訊請參閱說明文件。", + "storage_template_onboarding_description_v2": "啟用後,此功能將依據使用者自訂範本自動整理檔案。更多資訊請參閱說明文件。", "storage_template_path_length": "預估路徑長度上限:{length, number}/{limit, number}", "storage_template_settings": "儲存範本", - "storage_template_settings_description": "管理上傳檔案的資料夾結構和檔名", + "storage_template_settings_description": "管理上傳項目的資料夾結構與檔名", "storage_template_user_label": "{label} 是使用者的儲存標籤", "system_settings": "系統設定", "tag_cleanup_job": "清理標籤", - "template_email_available_tags": "您可以在您的範本中使用以下變數:{tags}", - "template_email_if_empty": "如果範本為空,將使用預設電子郵件範本。", + "template_email_available_tags": "您可以在範本中使用下列變數:{tags}", + "template_email_if_empty": "若範本內容為空,則會使用預設郵件範本。", "template_email_invite_album": "相簿邀請範本", "template_email_preview": "預覽", "template_email_settings": "電子郵件範本", @@ -351,13 +351,13 @@ "template_settings": "通知範本", "template_settings_description": "管理通知的自訂範本", "theme_custom_css_settings": "自訂 CSS", - "theme_custom_css_settings_description": "可以用層疊樣式表(CSS)來自訂 Immich 的設計。", + "theme_custom_css_settings_description": "透過階層式樣式表 (CSS) 即可自訂 Immich 的外觀設計。", "theme_settings": "主題設定", "theme_settings_description": "自訂 Immich 的網頁介面", "thumbnail_generation_job": "產生縮圖", - "thumbnail_generation_job_description": "為每個檔案產生大、小及模糊縮圖,也為每位人物產生縮圖", + "thumbnail_generation_job_description": "為每個項目產生大、小及模糊縮圖,也為每位人物產生縮圖", "transcoding_acceleration_api": "加速 API", - "transcoding_acceleration_api_description": "此 API 會使用您的硬體以加速轉碼流程。此設定採「盡力而為」模式——若轉碼失敗,將會回退至軟體轉碼。VP9 是否能運作,取決於您的硬體設定。", + "transcoding_acceleration_api_description": "此 API 將與您的裝置互動以加速轉碼。此設定採「盡力而為」模式:若失敗將回退至軟體轉碼。VP9 是否可用取決於硬體。", "transcoding_acceleration_nvenc": "NVENC(需要 NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync(需要第 7 代或更新的 Intel 處理器)", "transcoding_acceleration_rkmpp": "RKMPP(僅適用於 Rockchip SOCs)", @@ -367,17 +367,17 @@ "transcoding_accepted_containers": "可接受的封裝格式", "transcoding_accepted_containers_description": "選擇哪些封裝格式不需要重新封裝(remux)為 MP4。此設定僅適用於特定的轉碼策略。", "transcoding_accepted_video_codecs": "接受的影片編解碼器", - "transcoding_accepted_video_codecs_description": "選擇哪些視訊編解碼器不需要轉碼。此設定僅適用於特定的轉碼策略。", + "transcoding_accepted_video_codecs_description": "選擇哪些影片編解碼器不需要轉碼。此設定僅適用於特定的轉碼策略。", "transcoding_advanced_options_description": "大多數使用者不需更動的選項", "transcoding_audio_codec": "音訊編解碼器", - "transcoding_audio_codec_description": "是音質最佳的選項,但與舊裝置或舊版軟體的相容性較低。", + "transcoding_audio_codec_description": "Opus 是音質最佳的選項,但與舊裝置或舊版軟體的相容性較低。", "transcoding_bitrate_description": "位元率高於最大值或格式不在可接受範圍的影片", - "transcoding_codecs_learn_more": "如需進一步了解此處使用的術語,請參閱 FFmpeg 文件中關於 H.264 編解碼器HEVC 編解碼器VP9 編解碼器 的說明。", + "transcoding_codecs_learn_more": "如需進一步了解此處使用的術語,請參閱 FFmpeg 說明文件中關於 H.264 編解碼器HEVC 編解碼器VP9 編解碼器 的說明。", "transcoding_constant_quality_mode": "恆定品質模式", "transcoding_constant_quality_mode_description": "ICQ 的效果優於 CQP,但部分硬體加速裝置不支援此模式。設定此選項時,在使用以品質為基準的編碼時會優先採用所指定的模式。NVENC 不支援 ICQ,因此此設定在 NVENC 下會被忽略。", "transcoding_constant_rate_factor": "恆定速率因子(-crf)", "transcoding_constant_rate_factor_description": "視訊品質等級。典型值為 H.264 的 23、HEVC 的 28、VP9 的 31 和 AV1 的 35。數值越低,品質越好,但會產生較大的檔案。", - "transcoding_disabled_description": "不對任何影片進行轉碼,可能會導致部分用戶端無法正常播放", + "transcoding_disabled_description": "不對任何影片進行轉碼,這可能會導致部分用戶端無法正常播放", "transcoding_encoding_options": "編碼選項", "transcoding_encoding_options_description": "設定編碼影片的編解碼器、解析度、品質和其他選項", "transcoding_hardware_acceleration": "硬體加速", @@ -387,16 +387,16 @@ "transcoding_max_b_frames": "最大 B 幀數", "transcoding_max_b_frames_description": "較高的數值可提升壓縮效率,但會降低編碼速度。在較舊的裝置上,可能與硬體加速不相容。0 代表停用 B 幀,而 -1 則會自動設定此數值。", "transcoding_max_bitrate": "最大位元速率", - "transcoding_max_bitrate_description": "設定最大位元率可以在輕微犧牲品質的情況下,讓檔案大小更容易預測。在 720p 解析度下,VP9 或 HEVC 的典型值為 2600 kbit/s,H.264 則為 4500 kbit/s。設為 0 則停用此功能。當沒有指定組織時,假設k(代表kbit/s); 囙此,5000、5000k和5M(Mbit/s)是等效的。", + "transcoding_max_bitrate_description": "設定最大位元率能讓檔案大小更穩定,但會稍微犧牲品質。720p 下的典型值為:VP9 或 HEVC 為 2600 kbit/s,H.264 為 4500 kbit/s。設為 0 則停用。若未指定單位,系統將預設為 k (即 kbit/s);因此 5000、5000k 與 5M (即 Mbit/s) 是等效的。", "transcoding_max_keyframe_interval": "最大關鍵幀間隔", - "transcoding_max_keyframe_interval_description": "設定關鍵幀之間的最大幀距。較低的值會降低壓縮效率,但可以改善搜尋時間,並有可能會改善快速變動場景的品質。0 會自動設定此值。", + "transcoding_max_keyframe_interval_description": "設定關鍵幀之間的最大幀距。較低的數值會降低壓縮效率,但可改善跳轉搜尋時間並提升高動態場景品質。0 為自動設定。", "transcoding_optimal_description": "高於目標解析度或格式不在可接受範圍的影片", "transcoding_policy": "轉碼策略", "transcoding_policy_description": "設定影片進行轉碼的條件", "transcoding_preferred_hardware_device": "首選硬體裝置", "transcoding_preferred_hardware_device_description": "僅適用於 VAAPI 和 QSV。設定用於硬體轉碼的 dri 節點。", "transcoding_preset_preset": "預設值(-preset)", - "transcoding_preset_preset_description": "壓縮速度。較慢的預設值會產生較小的檔案,並在鎖定位元率時提升品質。VP9 在速度高於「faster」時將忽略設定。", + "transcoding_preset_preset_description": "壓縮速度。較慢的預設值可產生體積較小的檔案,並在指定位元率時提升品質。VP9 會忽略高於「faster」的設定。", "transcoding_reference_frames": "參考幀", "transcoding_reference_frames_description": "在壓縮特定幀時所參考的幀數量。數值越高可提升壓縮效率,但會降低編碼速度。設為 0 則自動決定此數值。", "transcoding_required_description": "僅限格式不被接受的影片", @@ -407,29 +407,29 @@ "transcoding_temporal_aq": "時間自適應量化(Temporal AQ)", "transcoding_temporal_aq_description": "僅適用於 NVENC,時域自我調整量化可提升高細節、低動態場景的畫質。可能與較舊的裝置不相容。", "transcoding_threads": "執行緒數量", - "transcoding_threads_description": "較高的值會加快編碼速度,但會減少伺服器在執行過程中處理其他任務的空間。此值不應超過 CPU 核心數。設定為 0 可以最大化利用率。", + "transcoding_threads_description": "較高的數值會加快編碼速度,但執行時會佔用更多伺服器處理其他任務的效能。此數值不應超過 CPU 核心數。設為 0 可最大化利用率。", "transcoding_tone_mapping": "色調對映", "transcoding_tone_mapping_description": "在將 HDR 影片轉換為 SDR 時,盡量維持原始觀感。每種演算法在色彩、細節和亮度方面都有不同的權衡。Hable 保留細節,Mobius 保留色彩,Reinhard 保留亮度。", "transcoding_transcode_policy": "轉碼策略", - "transcoding_transcode_policy_description": "影片何時應進行轉碼的策略。HDR 影片一定會轉碼(除非停用轉碼)。", + "transcoding_transcode_policy_description": "影片轉碼策略。HDR 影片一律會進行轉碼(除非停用轉碼功能)。", "transcoding_two_pass_encoding": "兩階段編碼", - "transcoding_two_pass_encoding_setting_description": "使用兩階段編碼來產生品質更佳的編碼影片。當啟用最大位元速率時(H.264 和 HEVC 必須啟用此選項才能運作),此模式會以最大位元速率來調整位元速率範圍,並忽略 CRF。對於 VP9,如果停用最大位元速率,可以使用 CRF。", + "transcoding_two_pass_encoding_setting_description": "執行兩次編碼以產生品質更佳的影片。啟用最大位元速率時(H.264 與 HEVC 必須啟用),此模式會依最大位元速率調整範圍並忽略 CRF。若為 VP9,則可在停用最大位元速率時使用 CRF。", "transcoding_video_codec": "影片編解碼器", "transcoding_video_codec_description": "VP9 具有高壓縮效率與良好的網頁相容性,但轉碼速度較慢。HEVC 的效能類似,但網頁相容性較差。H.264 具備廣泛的相容性且轉碼速度快,但產生的檔案體積較大。AV1 是效率最高的編解碼器,但在舊裝置上缺乏支援。", "trash_enabled_description": "啟用垃圾桶功能", "trash_number_of_days": "天數", - "trash_number_of_days_description": "媒體在垃圾桶中保留的天數,逾期後將永久刪除", + "trash_number_of_days_description": "項目在垃圾桶中保留的天數,逾期後將永久刪除", "trash_settings": "垃圾桶設定", "trash_settings_description": "管理垃圾桶設定", "unlink_all_oauth_accounts": "解除所有 OAuth 帳號的連結", "unlink_all_oauth_accounts_description": "在遷移至新的服務提供者前,請不要忘記要先解除所有與 OAuth 帳戶的連結。", - "unlink_all_oauth_accounts_prompt": "您是否確認要解除所有與 OAuth 帳戶的連結?所有相關的使用者身份會被重設,並且不能被還原。", + "unlink_all_oauth_accounts_prompt": "您確定要解除所有與 OAuth 帳號的連結嗎?這會重設每位使用者的 OAuth ID 且無法復原。", "user_cleanup_job": "清理使用者", "user_delete_delay": "{user} 的帳號和項目會在 {delay, plural, one {# 天} other {# 天}} 後永久刪除。", "user_delete_delay_settings": "延後刪除", - "user_delete_delay_settings_description": "自移除後起算的天數,逾期後將永久刪除使用者帳號與媒體。使用者刪除作業會在每日午夜執行,以檢查符合刪除條件的帳號。此設定的變更會在下一次執行時生效。", - "user_delete_immediately": "{user} 的帳號與媒體將立即排入永久刪除的佇列。", - "user_delete_immediately_checkbox": "立即將使用者與資產排入永久刪除佇列", + "user_delete_delay_settings_description": "自移除後起算的天數,逾期後將永久刪除使用者帳號與項目。使用者刪除作業會在每日午夜執行,以檢查符合刪除條件的帳號。此設定的變更將在下一次執行時生效。", + "user_delete_immediately": "{user} 的帳號與項目將立即排入永久刪除佇列。", + "user_delete_immediately_checkbox": "立即將使用者與項目排入永久刪除佇列", "user_details": "使用者詳細資訊", "user_management": "使用者管理", "user_password_has_been_reset": "使用者密碼已重設:", @@ -438,10 +438,10 @@ "user_restore_scheduled_removal": "還原使用者 - 預定於 {date, date, long} 移除", "user_settings": "使用者設定", "user_settings_description": "管理使用者設定", - "user_successfully_removed": "用戶{email}已成功删除。", - "users_page_description": "管理用戶頁面", + "user_successfully_removed": "已成功刪除使用者 {email}。", + "users_page_description": "管理使用者頁面", "version_check_enabled_description": "啟用版本檢查", - "version_check_implications": "版本檢查功能會定期與 github.com 通訊", + "version_check_implications": "版本檢查功能仰賴與 github.com 的定期通訊", "version_check_settings": "版本檢查", "version_check_settings_description": "啟用 / 停用新版本通知", "video_conversion_job": "影片轉碼", @@ -449,23 +449,23 @@ }, "admin_email": "管理員電子郵件", "admin_password": "管理員密碼", - "administration": "管理", + "administration": "系統管理", "advanced": "進階", "advanced_settings_clear_image_cache": "清除圖片快取", "advanced_settings_clear_image_cache_error": "清除圖片快取失敗", "advanced_settings_clear_image_cache_success": "成功清除{size}", "advanced_settings_enable_alternate_media_filter_subtitle": "使用此選項可在同步時依其他條件篩選媒體。僅在應用程式無法偵測到所有相簿時再嘗試使用。", "advanced_settings_enable_alternate_media_filter_title": "[實驗性] 使用替代的裝置相簿同步篩選器", - "advanced_settings_log_level_title": "日誌等級:{level}", - "advanced_settings_prefer_remote_subtitle": "部分裝置從本機媒體庫載入縮圖的速度非常慢。啟用此設定可改為載入遠端圖片。", + "advanced_settings_log_level_title": "紀錄等級:{level}", + "advanced_settings_prefer_remote_subtitle": "部分裝置從本機項目載入縮圖的速度非常慢。啟用此設定可改為載入遠端圖片。", "advanced_settings_prefer_remote_title": "偏好遠端影像", "advanced_settings_proxy_headers_subtitle": "定義 Immich 在每次網路請求時應該傳送的代理標頭", "advanced_settings_proxy_headers_title": "自定義代理標頭[實驗性]", - "advanced_settings_readonly_mode_subtitle": "開啟唯讀模式後,照片只能瀏覽,像是多選影像、分享、投放、刪除等功能都會關閉。可在主畫面透過使用者頭像來開啟/關閉唯讀模式", + "advanced_settings_readonly_mode_subtitle": "啟用唯讀模式後僅能瀏覽相片,將停用多選、分享、投放及刪除等功能。可透過主畫面上的使用者個人圖示啟用或停用唯讀模式", "advanced_settings_readonly_mode_title": "唯讀模式", "advanced_settings_self_signed_ssl_subtitle": "略過伺服器端點的 SSL 憑證驗證。自簽憑證時必須啟用此設定。", "advanced_settings_self_signed_ssl_title": "允許自簽的 SSL 憑證[實驗性]", - "advanced_settings_sync_remote_deletions_subtitle": "當在網頁端執行刪除或還原操作時,自動在此裝置上刪除或還原該媒體", + "advanced_settings_sync_remote_deletions_subtitle": "當在網頁端執行刪除或還原操作時,自動在此裝置上刪除或還原該項目", "advanced_settings_sync_remote_deletions_title": "同步遠端刪除 [實驗性]", "advanced_settings_tile_subtitle": "進階使用者設定", "advanced_settings_troubleshooting_subtitle": "啟用額外功能以進行疑難排解", @@ -474,14 +474,14 @@ "age_year_months": "1 歲,{months, plural, one {# 個月} other {# 個月}}", "age_years": "{years, plural, other {# 歲}}", "album": "相簿", - "album_added": "被加入到相簿", - "album_added_notification_setting_description": "當我被加入共享相簿時,用電子郵件通知我", + "album_added": "已加入相簿", + "album_added_notification_setting_description": "當我被加入共享相簿時,透過電子郵件通知我", "album_cover_updated": "已更新相簿封面", "album_delete_confirmation": "你確定要刪除相簿 {album} 嗎?", - "album_delete_confirmation_description": "如果此相簿已被分享,其他使用者將無法再存取。", + "album_delete_confirmation_description": "如果此相簿已被共享,其他使用者將無法再存取。", "album_deleted": "相簿已刪除", "album_info_card_backup_album_excluded": "已排除", - "album_info_card_backup_album_included": "已選中", + "album_info_card_backup_album_included": "已包含", "album_info_updated": "已更新相簿資訊", "album_leave": "離開相簿?", "album_leave_confirmation": "您確定要離開 {album} 嗎?", @@ -490,12 +490,12 @@ "album_remove_user": "移除使用者?", "album_remove_user_confirmation": "確定要移除 {user} 嗎?", "album_search_not_found": "找不到符合搜尋條件的相簿", - "album_selected": "已選擇相册", + "album_selected": "已選取相簿", "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", "album_summary": "相簿摘要", "album_updated": "更新相簿時", "album_updated_setting_description": "當共享相簿有新項目時用電子郵件通知我", - "album_upload_assets": "從您的計算機上傳文件並添加到相冊", + "album_upload_assets": "從您的電腦上傳檔案並加入相簿", "album_user_left": "離開 {album}", "album_user_removed": "移除 {user}", "album_viewer_appbar_delete_confirm": "您確定要從帳號中刪除此相簿嗎?", @@ -506,18 +506,18 @@ "album_viewer_appbar_share_leave": "離開相簿", "album_viewer_appbar_share_to": "分享給", "album_viewer_page_share_add_users": "邀請其他人", - "album_with_link_access": "任何擁有連結的人都能檢視此相簿中的照片與人物。", + "album_with_link_access": "任何擁有連結的人皆可檢視此相簿中的相片與人物。", "albums": "相簿", "albums_count": "{count, plural, one {{count, number} 個相簿} other {{count, number} 個相簿}}", "albums_default_sort_order": "預設相簿排序", "albums_default_sort_order_description": "建立新相簿時要初始化項目排序方式。", - "albums_feature_description": "一系列可以分享給其他使用者的項目。", + "albums_feature_description": "可共享給其他使用者的項目集合。", "albums_on_device_count": "此裝置有 ({count}) 個相簿", - "albums_selected": "{count, plural, one {# 個已選擇專輯} other {# 個已選擇專輯}}", + "albums_selected": "{count, plural, one {已選取 # 本相簿} other {已選取 # 本相簿}}", "all": "全部", "all_albums": "所有相簿", "all_people": "所有人物", - "all_photos": "所有照片", + "all_photos": "所有相片", "all_videos": "所有影片", "allow_dark_mode": "允許深色模式", "allow_edits": "允許編輯", @@ -526,27 +526,27 @@ "allowed": "允許", "alt_text_qr_code": "QR code 圖片", "always_keep": "一律保留", - "always_keep_photos_hint": "所有的照片將會被保留在此裝置上。", - "always_keep_videos_hint": "所有的影片將會被保留在此裝置上。", + "always_keep_photos_hint": "「釋放空間」功能會將所有相片保留在此裝置上。", + "always_keep_videos_hint": "「釋放空間」功能會將所有影片保留在此裝置上。", "anti_clockwise": "逆時針", "api_key": "API 金鑰", - "api_key_description": "此金鑰僅顯示一次。請在關閉前複製它。", - "api_key_empty": "您的 API 金鑰名稱不能為空值", + "api_key_description": "此金鑰僅會顯示一次。關閉視窗前請務必先複製金鑰。", + "api_key_empty": "API 金鑰名稱不得為空白", "api_keys": "API 金鑰", - "app_architecture_variant": "變體(架構)", + "app_architecture_variant": "變化版本(架構)", "app_bar_signout_dialog_content": "您確定要登出嗎?", "app_bar_signout_dialog_ok": "是", "app_bar_signout_dialog_title": "登出", - "app_download_links": "應用下載連結", + "app_download_links": "App 下載連結", "app_settings": "應用程式設定", - "app_stores": "應用商店", - "app_update_available": "應用程序更新可用", + "app_stores": "應用程式商店", + "app_update_available": "已有應用程式更新", "appears_in": "出現於", - "apply_count": "應用 ({count, number})", + "apply_count": "套用 ({count, number})", "archive": "封存", - "archive_action_prompt": "已將 ({count}) 個加入進封存", - "archive_or_unarchive_photo": "封存或取消封存照片", - "archive_page_no_archived_assets": "未找到封存媒體", + "archive_action_prompt": "已將 {count} 個項目加入封存", + "archive_or_unarchive_photo": "封存或取消封存相片", + "archive_page_no_archived_assets": "找不到封存項目", "archive_page_title": "封存 ({count})", "archive_size": "封存大小", "archive_size_description": "設定要下載的封存檔案大小 (單位:GiB)", @@ -554,60 +554,60 @@ "archived_count": "{count, plural, other {已封存 # 個項目}}", "are_these_the_same_person": "同一位人物?", "are_you_sure_to_do_this": "您確定嗎?", - "array_field_not_fully_supported": "數組欄位需要手動JSON編輯", - "asset_action_delete_err_read_only": "略過無法刪除唯讀項目", - "asset_action_share_err_offline": "略過無法取得的離線項目", - "asset_added_to_album": "已建立相簿", + "array_field_not_fully_supported": "陣列欄位需要手動編輯 JSON", + "asset_action_delete_err_read_only": "唯讀項目無法刪除,已略過", + "asset_action_share_err_offline": "無法取得離線項目,已略過", + "asset_added_to_album": "已新增至相簿", "asset_adding_to_album": "新增到相簿…", - "asset_created": "資產已創建", - "asset_description_updated": "媒體描述已更新", - "asset_filename_is_offline": "媒體 {filename} 已離線", - "asset_has_unassigned_faces": "媒體有未分配的臉孔", + "asset_created": "項目已建立", + "asset_description_updated": "項目說明已更新", + "asset_filename_is_offline": "項目 {filename} 已離線", + "asset_has_unassigned_faces": "項目有未指派的臉孔", "asset_hashing": "正在計算雜湊…", "asset_list_group_by_sub_title": "分類方式", "asset_list_layout_settings_dynamic_layout_title": "動態版面", "asset_list_layout_settings_group_automatically": "自動", - "asset_list_layout_settings_group_by": "媒體分類方式", + "asset_list_layout_settings_group_by": "項目分類方式", "asset_list_layout_settings_group_by_month_day": "月份和日期", "asset_list_layout_sub_title": "版面", "asset_list_settings_subtitle": "相片格狀版面設定", "asset_list_settings_title": "相片格狀檢視", "asset_not_found_on_device_android": "無法在裝置上找到項目", - "asset_not_found_on_device_ios": "無法在裝置上找到項目。iCloud 上的項目可能因檔案損失無法查閱", - "asset_not_found_on_icloud": "項目不存在於在iCloud。項目有機會因檔案損毀而無法檢閱", - "asset_offline": "媒體離線", - "asset_offline_description": "此外部媒體已無法在磁碟中找到。請聯絡您的 Immich 管理員以取得協助。", - "asset_restored_successfully": "媒體復原成功", + "asset_not_found_on_device_ios": "無法在裝置上找到項目。iCloud 上的項目可能因檔案損毀而無法查閱", + "asset_not_found_on_icloud": "iCloud 上找不到項目。該項目可能因檔案毀損而無法存取", + "asset_offline": "項目離線", + "asset_offline_description": "此外部項目已無法在磁碟上找到。請聯絡您的 Immich 管理員以取得協助。", + "asset_restored_successfully": "項目還原成功", "asset_skipped": "已跳過", "asset_skipped_in_trash": "已在垃圾桶", - "asset_trashed": "資產被丟棄", - "asset_troubleshoot": "資產故障排除", + "asset_trashed": "項目已移至垃圾桶", + "asset_troubleshoot": "項目故障排除", "asset_uploaded": "已上傳", "asset_uploading": "上傳中…", "asset_viewer_settings_subtitle": "管理您的媒體庫檢視器設定", - "asset_viewer_settings_title": "媒體檢視器", - "assets": "媒體", - "assets_added_count": "已新增 {count, plural, one {# 個媒體} other {# 個媒體}}", - "assets_added_to_album_count": "已將 {count, plural, one {# 個媒體} other {# 個媒體}}加入相簿", - "assets_added_to_albums_count": "已新增 {assetTotal, plural, one {# 個} other {# 個}}項目到 {albumTotal, plural, one {# 個} other {# 個}}相簿中", - "assets_cannot_be_added_to_album_count": "無法將 {count, plural, one {媒體} other {媒體}} 加入至相簿", - "assets_cannot_be_added_to_albums": "{count, plural, one {個} other {個}}項目無法被加入相簿", - "assets_count": "{count, plural, one {# 個媒體} other {# 個媒體}}", - "assets_deleted_permanently": "{count} 個媒體已被永久刪除", - "assets_deleted_permanently_from_server": "已從 Immich 伺服器中永久移除 {count} 個媒體", + "asset_viewer_settings_title": "項目檢視器", + "assets": "項目", + "assets_added_count": "已新增 {count, plural, one {# 個項目} other {# 個項目}}", + "assets_added_to_album_count": "已將 {count, plural, one {# 個項目} other {# 個項目}}加入至相簿", + "assets_added_to_albums_count": "已將 {assetTotal, plural, other {# 個項目}} 新增至 {albumTotal, plural, other {# 本相簿}}", + "assets_cannot_be_added_to_album_count": "無法將 {count, plural, one {項目} other {項目}} 加入至相簿", + "assets_cannot_be_added_to_albums": "無法將 {count, plural, other {# 個項目}} 加入任何相簿", + "assets_count": "{count, plural, one {# 個項目} other {# 個項目}}", + "assets_deleted_permanently": "已永久刪除 {count} 個項目", + "assets_deleted_permanently_from_server": "已從 Immich 伺服器中永久移除 {count} 個項目", "assets_downloaded_failed": "{count, plural, one {已下載 # 個檔案 - {error} 個檔案失敗} other {已下載 # 個檔案 - {error} 個檔案失敗}}", "assets_downloaded_successfully": "{count, plural, one {已成功下載 # 個檔案} other {已成功下載 # 個檔案}}", - "assets_moved_to_trash_count": "已將 {count, plural, one {# 個媒體} other {# 個媒體}}移動進垃圾桶", - "assets_permanently_deleted_count": "已永久刪除 {count, plural, one {# 個媒體} other {# 個媒體}}", - "assets_removed_count": "已移除 {count, plural, one {# 個媒體} other {# 個媒體}}", - "assets_removed_permanently_from_device": "已從您的裝置永久移除 {count} 個媒體", - "assets_restore_confirmation": "您確定要還原所有垃圾桶中的媒體嗎?此操作無法復原!請注意,任何離線媒體都無法透過此方式還原。", - "assets_restored_count": "已還原 {count, plural, one {# 個媒體} other {# 個媒體}}", - "assets_restored_successfully": "已成功還原 {count} 個媒體", - "assets_trashed": "已將 {count} 個媒體移至垃圾桶", - "assets_trashed_count": "已將 {count, plural, one {# 個媒體} other {# 個媒體}} 移至垃圾桶", - "assets_trashed_from_server": "已從 Immich 伺服器將 {count} 個媒體移至垃圾桶", - "assets_were_part_of_album_count": "{count, plural, one {該媒體已} other {這些媒體已}}在相簿中", + "assets_moved_to_trash_count": "已將 {count, plural, one {# 個項目} other {# 個項目}}移至垃圾桶", + "assets_permanently_deleted_count": "已永久刪除 {count, plural, one {# 個項目} other {# 個項目}}", + "assets_removed_count": "已移除 {count, plural, one {# 個項目} other {# 個項目}}", + "assets_removed_permanently_from_device": "已從您的裝置永久移除 {count} 個項目", + "assets_restore_confirmation": "您確定要還原所有垃圾桶中的項目嗎?此操作無法復原!請注意,任何離線項目都無法透過此方式還原。", + "assets_restored_count": "已還原 {count, plural, one {# 個項目} other {# 個項目}}", + "assets_restored_successfully": "已成功還原 {count} 個項目", + "assets_trashed": "已將 {count} 個項目移至垃圾桶", + "assets_trashed_count": "已將 {count, plural, one {# 個項目} other {# 個項目}}移至垃圾桶", + "assets_trashed_from_server": "已從 Immich 伺服器將 {count} 個項目移至垃圾桶", + "assets_were_part_of_album_count": "{count, plural, one {該項目已} other {這些項目已}}在相簿中", "assets_were_part_of_albums_count": "{count, plural, one {個} other {個}}項目已被儲存在相簿中", "authorized_devices": "已授權裝置", "automatic_endpoint_switching_subtitle": "當可用時,透過指定的 Wi-Fi 在本機連線,其他情況則使用替代連線", @@ -622,19 +622,19 @@ "backup": "備份", "backup_album_selection_page_albums_device": "裝置上的相簿({count})", "backup_album_selection_page_albums_tap": "點一下以選取,點兩下以排除", - "backup_album_selection_page_assets_scatter": "媒體可以分散在多個相簿中,因此在備份過程中可以選擇納入或排除相簿。", + "backup_album_selection_page_assets_scatter": "項目可以分散在多個相簿中,因此在備份過程中可以選擇納入或排除相簿。", "backup_album_selection_page_select_albums": "選取相簿", "backup_album_selection_page_selection_info": "選取資訊", - "backup_album_selection_page_total_assets": "總不重複媒體數", + "backup_album_selection_page_total_assets": "總不重複項目數", "backup_albums_sync": "備份相簿同步", "backup_all": "全部", - "backup_background_service_backup_failed_message": "備份媒體失敗。正在重試…", - "backup_background_service_complete_notification": "資產備份完成", + "backup_background_service_backup_failed_message": "備份項目失敗。正在重試…", + "backup_background_service_complete_notification": "項目備份完成", "backup_background_service_connection_failed_message": "連線至伺服器失敗。正在重試…", "backup_background_service_current_upload_notification": "正在上傳 {filename}", - "backup_background_service_default_notification": "正在檢查新媒體…", + "backup_background_service_default_notification": "正在檢查新項目…", "backup_background_service_error_title": "備份錯誤", - "backup_background_service_in_progress_notification": "正在備份您的媒體…", + "backup_background_service_in_progress_notification": "正在備份您的項目…", "backup_background_service_upload_failure_notification": "{filename} 上傳失敗", "backup_controller_page_albums": "備份相簿", "backup_controller_page_background_app_refresh_disabled_content": "請在「設定」>「一般」>「背景 App 重新整理」中啟用,以使用背景備份功能。", @@ -646,18 +646,18 @@ "backup_controller_page_background_battery_info_title": "電池最佳化", "backup_controller_page_background_charging": "僅在充電時", "backup_controller_page_background_configure_error": "背景服務設定失敗", - "backup_controller_page_background_delay": "新媒體備份延遲:{duration}", - "backup_controller_page_background_description": "開啟背景服務,即可在不需開啟 App 的情況下,自動備份所有新媒體", + "backup_controller_page_background_delay": "新項目備份延遲:{duration}", + "backup_controller_page_background_description": "開啟背景服務,即可在不需開啟 App 的情況下,自動備份所有新項目", "backup_controller_page_background_is_off": "背景自動備份已關閉", "backup_controller_page_background_is_on": "背景自動備份已開啟", "backup_controller_page_background_turn_off": "關閉背景服務", "backup_controller_page_background_turn_on": "開啟背景服務", "backup_controller_page_background_wifi": "僅使用 Wi-Fi", "backup_controller_page_backup": "備份", - "backup_controller_page_backup_selected": "已選中: ", - "backup_controller_page_backup_sub": "已備份的照片和影片", + "backup_controller_page_backup_selected": "已選取: ", + "backup_controller_page_backup_sub": "已備份的相片與影片", "backup_controller_page_created": "建立時間:{date}", - "backup_controller_page_desc_backup": "開啟前臺備份,在開啟 App 時自動將新媒體上傳至伺服器。", + "backup_controller_page_desc_backup": "開啟前景備份,在開啟 App 時自動將新項目上傳至伺服器。", "backup_controller_page_excluded": "已排除: ", "backup_controller_page_failed": "失敗({count})", "backup_controller_page_filename": "檔案名稱:{filename} [{size}]", @@ -665,20 +665,20 @@ "backup_controller_page_info": "備份資訊", "backup_controller_page_none_selected": "未選取任何項目", "backup_controller_page_remainder": "剩餘", - "backup_controller_page_remainder_sub": "選取項目中尚未備份的照片與影片", + "backup_controller_page_remainder_sub": "選取項目中尚未備份的相片與影片", "backup_controller_page_server_storage": "伺服器儲存空間", "backup_controller_page_start_backup": "開始備份", "backup_controller_page_status_off": "前臺自動備份已關閉", "backup_controller_page_status_on": "前臺自動備份已開啟", "backup_controller_page_storage_format": "{used} / {total} 已使用", "backup_controller_page_to_backup": "要備份的相簿", - "backup_controller_page_total_sub": "已選取相簿中的所有不重複的照片與影片", + "backup_controller_page_total_sub": "已選取相簿中的所有不重複的相片與影片", "backup_controller_page_turn_off": "關閉前臺備份", "backup_controller_page_turn_on": "開啟前臺備份", "backup_controller_page_uploading_file_info": "上傳中的檔案資訊", "backup_err_only_album": "不能移除唯一的相簿", "backup_error_sync_failed": "同步失敗,無法處理備份。", - "backup_info_card_assets": "個媒體", + "backup_info_card_assets": "個項目", "backup_manual_cancelled": "已取消", "backup_manual_in_progress": "上傳正在進行中,請稍後再試", "backup_manual_success": "成功", @@ -690,23 +690,23 @@ "backup_upload_details_page_more_details": "點擊查看更多詳細資訊", "backward": "由舊至新", "biometric_auth_enabled": "生物辨識驗證已啟用", - "biometric_locked_out": "您已被鎖定無法使用生物辨識驗證", + "biometric_locked_out": "生物辨識驗證已被鎖定", "biometric_no_options": "沒有生物辨識選項可用", - "biometric_not_available": "此裝置上無法使用生物辨識驗證", + "biometric_not_available": "此裝置不支援生物辨識驗證", "birthdate_saved": "出生日期儲存成功", - "birthdate_set_description": "出生日期用於計算此人在照片拍攝時的年齡。", + "birthdate_set_description": "出生日期用於計算此人在相片拍攝時的年齡。", "blurred_background": "背景模糊", "bugs_and_feature_requests": "錯誤及功能請求", "build": "建置編號", "build_image": "建置映像", - "bulk_delete_duplicates_confirmation": "您確定要批次刪除 {count, plural, one {# 個重複媒體} other {# 個重複媒體}} 嗎?系統將保留每組中大小最大的媒體,並永久刪除所有其他重複項目。此操作無法復原!", - "bulk_keep_duplicates_confirmation": "您確定要保留 {count, plural, one {# 個重複媒體} other {# 個重複媒體}} 嗎?這將在不刪除任何項目的情況下解決所有重複群組。", - "bulk_trash_duplicates_confirmation": "您確定要批次將 {count, plural, one {# 個重複媒體} other {# 個重複媒體}}移至垃圾桶嗎?系統將保留每組中大小最大的媒體,並將所有其他重複項目移至垃圾桶。", + "bulk_delete_duplicates_confirmation": "您確定要批次刪除 {count, plural, one {# 個重複項目} other {# 個重複項目}} 嗎?系統將保留每組中容量最大的項目,並永久刪除所有其他重複項目。此動作無法復原!", + "bulk_keep_duplicates_confirmation": "您確定要保留 {count, plural, one {# 個重複項目} other {# 個重複項目}} 嗎?這將在不刪除任何內容的情況下解決所有重複群組。", + "bulk_trash_duplicates_confirmation": "您確定要批次將 {count, plural, one {# 個重複項目} other {# 個重複項目}}移至垃圾桶嗎?系統將保留每組中容量最大的項目,並將所有其他重複項目移至垃圾桶。", "buy": "購買 Immich", "cache_settings_clear_cache_button": "清除快取", "cache_settings_clear_cache_button_title": "清除 App 的快取。此動作會在快取重新建立前,顯著影響 App 的效能。", "cache_settings_duplicated_assets_clear_button": "清除", - "cache_settings_duplicated_assets_subtitle": "被應用程式加入忽略清單的照片與影片", + "cache_settings_duplicated_assets_subtitle": "被應用程式忽略清單中的相片與影片", "cache_settings_duplicated_assets_title": "重複項目({count})", "cache_settings_statistics_album": "媒體庫縮圖", "cache_settings_statistics_full": "完整圖片", @@ -735,42 +735,42 @@ "change_expiration_time": "變更到期時間", "change_location": "變更位置", "change_name": "變更名稱", - "change_name_successfully": "變更名稱成功", + "change_name_successfully": "名稱變更成功", "change_password": "變更密碼", "change_password_description": "這是您首次登入系統,或是已收到變更密碼的請求。請在下方輸入新密碼。", "change_password_form_confirm_password": "確認密碼", "change_password_form_description": "您好 {name},\n\n這是您首次登入系統,或是已收到變更密碼的請求。請在下方輸入新密碼。", - "change_password_form_log_out": "註銷所有其他設備", - "change_password_form_log_out_description": "建議退出所有其他設備", + "change_password_form_log_out": "登出所有其他裝置", + "change_password_form_log_out_description": "建議從所有其他裝置登出", "change_password_form_new_password": "新密碼", "change_password_form_password_mismatch": "密碼不一致", "change_password_form_reenter_new_password": "再次輸入新密碼", "change_pin_code": "變更 PIN 碼", "change_trigger": "更改觸發器", - "change_trigger_prompt": "您確定要更改觸發器嗎? 這將删除所有現有操作和篩選器。", + "change_trigger_prompt": "確定要變更觸發條件嗎?這將移除所有現有的動作與篩選器。", "change_your_password": "變更您的密碼", "changed_visibility_successfully": "已成功變更可見性", "charging": "充電", "charging_requirement_mobile_backup": "後臺備份要求裝置正在充電", "check_corrupt_asset_backup": "檢查損毀的備份項目", "check_corrupt_asset_backup_button": "執行檢查", - "check_corrupt_asset_backup_description": "僅在已連線至 Wi-Fi 且所有媒體已完成備份後執行此檢查。此程式可能需要數分鐘。", - "check_logs": "檢查日誌", - "checksum": "校驗和", + "check_corrupt_asset_backup_description": "僅在已連線至 Wi-Fi 且所有項目已完成備份後執行此檢查。此程式可能需要數分鐘。", + "check_logs": "檢查紀錄", + "checksum": "校驗碼", "choose_matching_people_to_merge": "選擇要合併的相符人物", "city": "城市", - "cleanup_confirm_description": "Immich 發現 {count} 個項目(在 {date} 之前創建)已安全備份到服務器。是否從此設備中刪除本地副本?", + "cleanup_confirm_description": "Immich 發現有 {count} 個項目(建立於 {date} 之前)已安全備份至伺服器。是否要從此裝置中刪除本機副本?", "cleanup_confirm_prompt_title": "從此裝置刪除?", "cleanup_deleted_assets": "已將{count}項目移到裝置的垃圾桶裡", "cleanup_deleting": "正在移動到垃圾桶...", "cleanup_found_assets": "找到{count}件已上傳的項目", "cleanup_found_assets_with_size": "找到{count}件,總共({size})已上傳的項目", "cleanup_icloud_shared_albums_excluded": "iCloud共享相簿被排除於搜尋之外", - "cleanup_no_assets_found": "未找到任何符合條件的項目。釋放內存功能只能移除已備份到伺服器的項目", + "cleanup_no_assets_found": "找不到符合上述條件的項目。釋放空間功能僅能移除已備份至伺服器的項目", "cleanup_preview_title": "{count} 項需要移除的項目", - "cleanup_step3_description": "掃描符合日期和保存設定的已備份項目。", - "cleanup_step4_summary": "從這台裝置上移除{count}件創建於{date}前的項目。照片仍然可以在Immich上查看。", - "cleanup_trash_hint": "要完全恢復內存,請清空相簿中的垃圾桶", + "cleanup_step3_description": "掃描符合日期與儲存設定的已備份項目。", + "cleanup_step4_summary": "將從此裝置移除 {count} 個建立於 {date} 之前的項目。您仍可透過 Immich 應用程式存取這些相片。", + "cleanup_trash_hint": "若要徹底釋放儲存空間,請開啟系統相簿 App 並清空垃圾桶", "clear": "清空", "clear_all": "全部清除", "clear_all_recent_searches": "清除所有最近的搜尋", @@ -782,9 +782,11 @@ "client_cert_import": "匯入", "client_cert_import_success_msg": "已匯入用戶端憑證", "client_cert_invalid_msg": "無效的憑證檔案或密碼錯誤", + "client_cert_password_message": "請輸入此證書的密碼", + "client_cert_password_title": "證書密碼", "client_cert_remove_msg": "用戶端憑證已移除", - "client_cert_subtitle": "僅支援 PKCS12 (.p12, .pfx) 格式。僅可在登入前進行憑證的匯入和移除", - "client_cert_title": "SSL 用戶端憑證[實驗性]", + "client_cert_subtitle": "僅支援 PKCS12 (.p12, .pfx) 格式。憑證匯入與移除僅可在登入前進行", + "client_cert_title": "SSL 用戶端憑證 [實驗性]", "clockwise": "順時針", "close": "關閉", "collapse": "折疊", @@ -792,6 +794,11 @@ "color": "顏色", "color_theme": "色彩主題", "command": "命令", + "command_palette_prompt": "快速尋找頁面,動作或者指令", + "command_palette_to_close": "關閉", + "command_palette_to_navigate": "輸入", + "command_palette_to_select": "選擇", + "command_palette_to_show_all": "顯示全部", "comment_deleted": "留言已刪除", "comment_options": "留言選項", "comments_and_likes": "留言與喜歡", @@ -800,9 +807,9 @@ "completed": "已完成", "confirm": "確認", "confirm_admin_password": "確認管理員密碼", - "confirm_delete_face": "您確定要從該媒體中刪除{name}的臉孔嗎?", - "confirm_delete_shared_link": "您確定要刪除這個共享連結嗎?", - "confirm_keep_this_delete_others": "除此媒體外,堆疊中的其他媒體都將被刪除。您確定要繼續嗎?", + "confirm_delete_face": "您確定要從該項目中刪除 {name} 的臉孔嗎?", + "confirm_delete_shared_link": "您確定要刪除此分享連結嗎?", + "confirm_keep_this_delete_others": "除此項目外,堆疊中的其他項目都將被刪除。您確定要繼續嗎?", "confirm_new_pin_code": "確認新 PIN 碼", "confirm_password": "確認密碼", "confirm_tag_face": "您想要將此臉孔標籤為 {name} 嗎?", @@ -835,23 +842,23 @@ "create": "建立", "create_album": "建立相簿", "create_album_page_untitled": "未命名", - "create_api_key": "創建API金鑰", - "create_first_workflow": "創建第一個工作流", + "create_api_key": "建立 API 金鑰", + "create_first_workflow": "建立第一個工作流程", "create_library": "建立媒體庫", "create_link": "建立連結", - "create_link_to_share": "建立共享連結", - "create_link_to_share_description": "任何持有連結的人都允許檢視所選相片", + "create_link_to_share": "建立分享連結", + "create_link_to_share_description": "持有連結的人皆可檢視所選項目", "create_new": "新增", "create_new_person": "建立新人物", - "create_new_person_hint": "將選定的媒體分配給新人物", + "create_new_person_hint": "將選取的項目指派給新的人物", "create_new_user": "建立新使用者", - "create_shared_album_page_share_add_assets": "新增膜體", - "create_shared_album_page_share_select_photos": "選擇照片", - "create_shared_link": "建立共享連結", + "create_shared_album_page_share_add_assets": "新增項目", + "create_shared_album_page_share_select_photos": "選取相片", + "create_shared_link": "建立分享連結", "create_tag": "建立標籤", "create_tag_description": "建立新標籤。若要建立巢狀標籤,請輸入包含正斜線的完整標籤路徑。", "create_user": "建立使用者", - "create_workflow": "創建工作流", + "create_workflow": "建立工作流程", "created": "建立於", "created_at": "建立於", "creating_linked_albums": "建立連結相簿 ...", @@ -867,7 +874,7 @@ "custom_locale": "自訂地區設定", "custom_locale_description": "根據語言與地區格式化日期與數字", "custom_url": "自訂 URL", - "cutoff_date_description": "保留最近多少天的照片…", + "cutoff_date_description": "保留最近多少天的相片…", "cutoff_day": "{count, plural, one {天} other {天}}", "cutoff_year": "{count, plural, one {年} other {年}}", "daily_title_text_date": "E, MMM dd", @@ -887,11 +894,11 @@ "deduplication_criteria_1": "影像大小(以位元組為單位)", "deduplication_criteria_2": "EXIF 資料數量", "deduplication_info": "重複資料刪除資訊", - "deduplication_info_description": "要自動預先選取媒體並批次移除重複項目,我們會檢查:", - "default_locale": "預設地區", + "deduplication_info_description": "若要自動預先選取項目並批次移除重複項目,我們會檢查:", + "default_locale": "預設地區設定", "default_locale_description": "依照您的瀏覽器地區設定格式化日期與數字", "delete": "刪除", - "delete_action_confirmation_message": "您確定要刪除此媒體嗎?此操作會將該媒體移至伺服器的垃圾桶,並會提示您是否要在本機同時刪除", + "delete_action_confirmation_message": "您確定要刪除此項目嗎?此動作會將該項目移至伺服器的垃圾桶,並詢問您是否要在本機同步刪除", "delete_action_prompt": "{count} 個已刪除", "delete_album": "刪除相簿", "delete_api_key_prompt": "您確定要刪除這個 API 金鑰嗎?", @@ -912,36 +919,36 @@ "delete_others": "刪除其他", "delete_permanently": "永久刪除", "delete_permanently_action_prompt": "已永久刪除 {count} 個項目", - "delete_shared_link": "刪除共享連結", - "delete_shared_link_dialog_title": "刪除共享連結", + "delete_shared_link": "刪除分享連結", + "delete_shared_link_dialog_title": "刪除分享連結", "delete_tag": "刪除標籤", "delete_tag_confirmation_prompt": "您確定要刪除「{tagName}」標籤嗎?", "delete_user": "刪除使用者", "deleted_shared_link": "共享連結已刪除", - "deletes_missing_assets": "刪除磁碟中遺失的媒體", + "deletes_missing_assets": "刪除磁碟中遺失的項目", "description": "描述", "description_input_hint_text": "新增描述...", - "description_input_submit_error": "更新描述時發生錯誤,請檢查日誌以取得更多詳細資訊", + "description_input_submit_error": "更新說明時發生錯誤,請檢查紀錄以取得更多詳細資訊", "deselect_all": "取消全選", "details": "詳細資訊", "direction": "方向", - "disable": "禁用", + "disable": "停用", "disabled": "已停用", "disallow_edits": "不允許編輯", "discord": "Discord", "discover": "探索", - "discovered_devices": "已探索的裝置", + "discovered_devices": "已發現的裝置", "dismiss_all_errors": "忽略所有錯誤", "dismiss_error": "忽略錯誤", "display_options": "顯示選項", "display_order": "顯示順序", - "display_original_photos": "顯示原始照片", - "display_original_photos_setting_description": "在檢視媒體時,若原始媒體與網頁相容,則優先顯示原始相片而非縮圖。這可能會導致照片顯示速度變慢。", + "display_original_photos": "顯示原始相片", + "display_original_photos_setting_description": "在檢視項目時,若原始項目與網頁相容,則優先顯示原始相片而非縮圖。這可能會導致相片載入速度變慢。", "do_not_show_again": "不再顯示此訊息", "documentation": "說明文件", "done": "完成", "download": "下載", - "download_action_prompt": "正在下載 {count} 個媒體", + "download_action_prompt": "正在下載 {count} 個項目", "download_canceled": "下載已取消", "download_complete": "下載完成", "download_enqueue": "已加入下載佇列", @@ -949,23 +956,23 @@ "download_failed": "下載失敗", "download_finished": "下載完成", "download_include_embedded_motion_videos": "嵌入影片", - "download_include_embedded_motion_videos_description": "將動態相片中內嵌的影片另存為獨立檔案", + "download_include_embedded_motion_videos_description": "將動態相片中內嵌的影片儲存為獨立檔案", "download_notfound": "無法找到下載", - "download_original": "下載原始文件", + "download_original": "下載原始檔案", "download_paused": "下載已暫停", "download_settings": "下載", - "download_settings_description": "管理與媒體下載相關的設定", + "download_settings_description": "管理與項目下載相關的設定", "download_started": "已開始下載", "download_sucess": "下載成功", "download_sucess_android": "媒體已下載至 DCIM/Immich", "download_waiting_to_retry": "等待重試", "downloading": "下載中", - "downloading_asset_filename": "正在下載媒體 {filename}", + "downloading_asset_filename": "正在下載項目 {filename}", "downloading_from_icloud": "正從iCloud下載", "downloading_media": "正在下載媒體", "drop_files_to_upload": "將檔案拖放到任何位置以上傳", "duplicates": "重複項目", - "duplicates_description": "逐一檢查每個群組,並標示其中是否有重複媒體", + "duplicates_description": "逐一檢查每個群組,並標示其中是否有重複項目", "duration": "顯示時長", "edit": "編輯", "edit_album": "編輯相簿", @@ -992,9 +999,14 @@ "edit_user": "編輯使用者", "edit_workflow": "編輯工作流程", "editor": "編輯器", - "editor_close_without_save_prompt": "此變更將不會被儲存", + "editor_close_without_save_prompt": "變更將不會被儲存", "editor_close_without_save_title": "要關閉編輯器嗎?", "editor_confirm_reset_all_changes": "你確定要重設所有變更嗎?", + "editor_discard_edits_confirm": "放棄編輯", + "editor_discard_edits_prompt": "您有尚未儲存的編輯內容。確定要捨棄嗎?", + "editor_discard_edits_title": "確認放棄編輯嗎?", + "editor_edits_applied_error": "無法套用編輯", + "editor_edits_applied_success": "已成功套用編輯", "editor_flip_horizontal": "水平翻轉", "editor_flip_vertical": "垂直翻轉", "editor_orientation": "方向", @@ -1005,7 +1017,7 @@ "email_notifications": "Email 通知", "empty_folder": "這個資料夾是空的", "empty_trash": "清空垃圾桶", - "empty_trash_confirmation": "您確定要清空垃圾桶嗎?這會永久刪除 Immich 垃圾桶中所有的媒體。\n您無法撤銷此變更!", + "empty_trash_confirmation": "您確定要清空垃圾桶嗎?這會從 Immich 永久移除垃圾桶中所有的項目。\n您無法復原此動作!", "enable": "啟用", "enable_backup": "啟用備份", "enable_biometric_auth_description": "輸入您的 PIN 碼以啟用生物辨識驗證", @@ -1014,95 +1026,95 @@ "enqueued": "已排入佇列", "enter_wifi_name": "輸入 Wi-Fi 名稱", "enter_your_pin_code": "輸入您的 PIN 碼", - "enter_your_pin_code_subtitle": "輸入您的 PIN 碼以存取鎖定的資料夾", + "enter_your_pin_code_subtitle": "輸入您的 PIN 碼以存取「已鎖定」資料夾", "error": "錯誤", "error_change_sort_album": "變更相簿排序失敗", - "error_delete_face": "從媒體刪除臉孔時失敗", + "error_delete_face": "從項目刪除臉孔時發生錯誤", "error_getting_places": "取得位置時出錯", - "error_loading_albums": "無法加載相簿", + "error_loading_albums": "載入相簿時發生錯誤", "error_loading_image": "圖片載入錯誤", - "error_loading_partners": "載入合作夥伴時出錯:{error}", - "error_retrieving_asset_information": "無法獲取項目資訊", + "error_loading_partners": "載入親友時發生錯誤:{error}", + "error_retrieving_asset_information": "無法取得項目資訊", "error_saving_image": "錯誤:{error}", "error_tag_face_bounding_box": "標記臉部錯誤 - 無法取得邊界框坐標", "error_title": "錯誤 - 發生錯誤", "error_while_navigating": "無法引導至項目", "errors": { - "cannot_navigate_next_asset": "無法導覽至下一個媒體", - "cannot_navigate_previous_asset": "無法導覽至上一個媒體", + "cannot_navigate_next_asset": "無法導覽至下一個項目", + "cannot_navigate_previous_asset": "無法導覽至上一個項目", "cant_apply_changes": "無法套用變更", "cant_change_activity": "無法{enabled, select, true {停用} other {啟用}}活動", - "cant_change_asset_favorite": "無法變更檔案的收藏狀態", - "cant_change_metadata_assets_count": "無法變更 {count, plural, other {# 個檔案}}的中繼資料", + "cant_change_asset_favorite": "無法變更項目的收藏狀態", + "cant_change_metadata_assets_count": "無法變更 {count, plural, other {# 個項目}} 的中繼資料", "cant_get_faces": "無法取得臉孔", "cant_get_number_of_comments": "無法取得留言數量", "cant_search_people": "無法搜尋人物", "cant_search_places": "無法搜尋地點", - "error_adding_assets_to_album": "將媒體加入相簿時發生錯誤", + "error_adding_assets_to_album": "將項目加入相簿時發生錯誤", "error_adding_users_to_album": "將使用者加入相簿時發生錯誤", "error_deleting_shared_user": "刪除共享使用者時發生錯誤", "error_downloading": "下載 {filename} 時發生錯誤", "error_hiding_buy_button": "隱藏購買按鈕時發生錯誤", - "error_removing_assets_from_album": "從相簿移除媒體時發生錯誤,請檢查主控臺以取得更多詳細資訊", + "error_removing_assets_from_album": "從相簿移除項目時發生錯誤,請檢查主控台以取得更多詳細資訊", "error_selecting_all_assets": "選取所有檔案時發生錯誤", "exclusion_pattern_already_exists": "此排除模式已存在。", "failed_to_create_album": "相簿建立失敗", - "failed_to_create_shared_link": "建立共享連結失敗", - "failed_to_edit_shared_link": "編輯共享連結失敗", + "failed_to_create_shared_link": "分享連結建立失敗", + "failed_to_edit_shared_link": "分享連結編輯失敗", "failed_to_get_people": "無法取得人物", - "failed_to_keep_this_delete_others": "無法保留此媒體並刪除其他媒體", - "failed_to_load_asset": "媒體載入失敗", - "failed_to_load_assets": "媒體載入失敗", + "failed_to_keep_this_delete_others": "無法保留此項目並刪除其他項目", + "failed_to_load_asset": "項目載入失敗", + "failed_to_load_assets": "項目載入失敗", "failed_to_load_notifications": "載入通知失敗", "failed_to_load_people": "載入人物失敗", "failed_to_remove_product_key": "移除產品金鑰失敗", "failed_to_reset_pin_code": "重設 PIN 碼失敗", - "failed_to_stack_assets": "無法媒體堆疊", - "failed_to_unstack_assets": "解除媒體堆疊失敗", + "failed_to_stack_assets": "項目堆疊失敗", + "failed_to_unstack_assets": "解除項目堆疊失敗", "failed_to_update_notification_status": "無法更新通知狀態", "incorrect_email_or_password": "電子郵件或密碼錯誤", - "library_folder_already_exists": "此導入路徑已存在。", + "library_folder_already_exists": "此匯入路徑已存在。", "paths_validation_failed": "{paths, plural, one {# 個路徑} other {# 個路徑}} 驗證失敗", "profile_picture_transparent_pixels": "個人資料圖片不能有透明畫素。請放大並/或移動影像。", - "quota_higher_than_disk_size": "您所設定的配額大於磁碟大小", + "quota_higher_than_disk_size": "您設定的配額大於磁碟容量", "something_went_wrong": "發生錯誤", "unable_to_add_album_users": "無法將使用者加入相簿", - "unable_to_add_assets_to_shared_link": "無法加入媒體到共享連結", + "unable_to_add_assets_to_shared_link": "無法將項目加入至分享連結", "unable_to_add_comment": "無法新增留言", - "unable_to_add_exclusion_pattern": "無法新增篩選條件", - "unable_to_add_partners": "無法新增親朋好友", - "unable_to_add_remove_archive": "無法{archived, select, true {從封存中移除媒體} other {將檔案加入媒體}}", - "unable_to_add_remove_favorites": "無法將媒體{favorite, select, true {加入收藏} other {從收藏中移除}}", + "unable_to_add_exclusion_pattern": "無法新增排除模式", + "unable_to_add_partners": "無法新增親友", + "unable_to_add_remove_archive": "無法將項目{archived, select, true {從封存中移除} other {加入至封存}}", + "unable_to_add_remove_favorites": "無法將項目{favorite, select, true {加入收藏} other {從收藏中移除}}", "unable_to_archive_unarchive": "無法{archived, select, true {封存} other {取消封存}}", "unable_to_change_album_user_role": "無法變更相簿使用者的角色", "unable_to_change_date": "無法變更日期", "unable_to_change_description": "無法變更描述", - "unable_to_change_favorite": "無法變更媒體的收藏狀態", + "unable_to_change_favorite": "無法變更項目的收藏狀態", "unable_to_change_location": "無法變更位置", "unable_to_change_password": "無法變更密碼", "unable_to_change_visibility": "無法變更 {count, plural, one {# 位人物} other {# 位人物}} 的可見性", "unable_to_complete_oauth_login": "無法完成 OAuth 登入", "unable_to_connect": "無法連線", - "unable_to_copy_to_clipboard": "無法複製到剪貼簿,請確保您是以 https 存取本頁面", - "unable_to_create": "無法創建工作流", + "unable_to_copy_to_clipboard": "無法複製到剪貼簿,請確保您正透過 https 存取此頁面", + "unable_to_create": "無法建立工作流程", "unable_to_create_admin_account": "無法建立管理員帳號", "unable_to_create_api_key": "無法建立新的 API 金鑰", "unable_to_create_library": "無法建立媒體庫", "unable_to_create_user": "無法建立使用者", "unable_to_delete_album": "無法刪除相簿", - "unable_to_delete_asset": "無法刪除媒體", - "unable_to_delete_assets": "刪除媒體時發生錯誤", + "unable_to_delete_asset": "無法刪除項目", + "unable_to_delete_assets": "刪除項目時發生錯誤", "unable_to_delete_exclusion_pattern": "無法刪除篩選條件", - "unable_to_delete_shared_link": "刪除共享連結失敗", + "unable_to_delete_shared_link": "無法刪除分享連結", "unable_to_delete_user": "無法刪除使用者", - "unable_to_delete_workflow": "無法删除工作流", + "unable_to_delete_workflow": "無法刪除工作流程", "unable_to_download_files": "無法下載檔案", "unable_to_edit_exclusion_pattern": "無法編輯篩選條件", "unable_to_empty_trash": "無法清空垃圾桶", "unable_to_enter_fullscreen": "無法進入全螢幕", "unable_to_exit_fullscreen": "無法結束全螢幕", "unable_to_get_comments_number": "無法取得留言數量", - "unable_to_get_shared_link": "取得共享連結失敗", + "unable_to_get_shared_link": "取得分享連結失敗", "unable_to_hide_person": "無法隱藏人物", "unable_to_link_motion_video": "無法連結動態影片", "unable_to_link_oauth_account": "無法連結 OAuth 帳號", @@ -1110,19 +1122,19 @@ "unable_to_log_out_device": "無法登出裝置", "unable_to_login_with_oauth": "無法使用 OAuth 登入", "unable_to_play_video": "無法播放影片", - "unable_to_reassign_assets_existing_person": "無法將檔案重新指派給 {name, select, null {現有的人員} other {{name}}}", - "unable_to_reassign_assets_new_person": "無法將媒體重新指派給新的人物", + "unable_to_reassign_assets_existing_person": "無法將項目重新指派給 {name, select, null {現有人物} other {{name}}}", + "unable_to_reassign_assets_new_person": "無法將項目重新指派給新的人物", "unable_to_refresh_user": "無法重新整理使用者", "unable_to_remove_album_users": "無法從相簿中移除使用者", "unable_to_remove_api_key": "無法移除 API 金鑰", - "unable_to_remove_assets_from_shared_link": "刪除共享連結中媒體失敗", + "unable_to_remove_assets_from_shared_link": "無法從分享連結中移除項目", "unable_to_remove_library": "無法移除媒體庫", - "unable_to_remove_partner": "無法移除親朋好友", + "unable_to_remove_partner": "無法移除親友", "unable_to_remove_reaction": "無法移除反應", "unable_to_reset_password": "無法重設密碼", "unable_to_reset_pin_code": "無法重設 PIN 碼", "unable_to_resolve_duplicate": "無法解決重複項目", - "unable_to_restore_assets": "無法還原媒體", + "unable_to_restore_assets": "無法還原項目", "unable_to_restore_trash": "無法還原垃圾桶", "unable_to_restore_user": "無法還原使用者", "unable_to_save_album": "無法儲存相簿", @@ -1133,11 +1145,11 @@ "unable_to_save_settings": "無法儲存設定", "unable_to_scan_libraries": "無法掃描媒體庫", "unable_to_scan_library": "無法掃描媒體庫", - "unable_to_set_feature_photo": "無法設定封面圖片", + "unable_to_set_feature_photo": "無法設定精選相片", "unable_to_set_profile_picture": "無法設定個人資料圖片", "unable_to_set_rating": "無法設定評星", "unable_to_submit_job": "無法提交任務", - "unable_to_trash_asset": "無法將媒體丟進垃圾桶", + "unable_to_trash_asset": "無法將項目移至垃圾桶", "unable_to_unlink_account": "無法解除帳號連結", "unable_to_unlink_motion_video": "無法解除連結動態影片", "unable_to_update_album_cover": "無法更新相簿封面", @@ -1147,12 +1159,12 @@ "unable_to_update_settings": "無法更新設定", "unable_to_update_timeline_display_status": "無法更新時間軸顯示狀態", "unable_to_update_user": "無法更新使用者", - "unable_to_update_workflow": "無法更新工作流", + "unable_to_update_workflow": "無法更新工作流程", "unable_to_upload_file": "無法上傳檔案" }, "errors_text": "錯誤", "exclusion_pattern": "排除模式", - "exif": "EXIF 可交換影像檔格式", + "exif": "EXIF", "exif_bottom_sheet_description": "新增描述...", "exif_bottom_sheet_description_error": "更新描述時發生錯誤", "exif_bottom_sheet_details": "詳細資料", @@ -1161,6 +1173,7 @@ "exif_bottom_sheet_people": "人物", "exif_bottom_sheet_person_add_person": "新增姓名", "exit_slideshow": "結束幻燈片", + "expand": "展開", "expand_all": "展開全部", "experimental_settings_new_asset_list_subtitle": "正在處理", "experimental_settings_new_asset_list_title": "啟用實驗性相片格狀版面", @@ -1179,32 +1192,34 @@ "external": "外部", "external_libraries": "外部媒體庫", "external_network": "外部網路", - "external_network_sheet_info": "若未連線至偏好的 Wi-Fi,將依列表從上到下選擇可連線的伺服器網址", - "face_unassigned": "未指定", + "external_network_sheet_info": "若未連網至偏好的 Wi-Fi,將依清單從上到下選擇可連線的伺服器網址", + "face_unassigned": "未指派", "failed": "失敗", "failed_count": "失敗:{count}", "failed_to_authenticate": "身份驗證失敗", - "failed_to_load_assets": "無法載入媒體", + "failed_to_load_assets": "項目載入失敗", "failed_to_load_folder": "無法載入資料夾", "favorite": "收藏", - "favorite_action_prompt": "已新增 {count} 個到收藏", - "favorite_or_unfavorite_photo": "收藏或取消收藏照片", + "favorite_action_prompt": "已將 {count} 個項目加入收藏", + "favorite_or_unfavorite_photo": "收藏或取消收藏相片", "favorites": "收藏", "favorites_page_no_favorites": "未找到收藏項目", - "feature_photo_updated": "特色照片已更新", + "feature_photo_updated": "精選相片已更新", "features": "功能", "features_in_development": "發展中的特點", "features_setting_description": "管理應用程式功能", "file_name_or_extension": "檔案名稱或副檔名", - "file_size": "文件大小", + "file_name_text": "檔案名稱", + "file_name_with_value": "檔案名稱: {file_name}", + "file_size": "檔案大小", "filename": "檔案名稱", "filetype": "檔案類型", "filter": "濾鏡", - "filter_description": "篩選目標資產的條件", + "filter_description": "篩選目標項目的條件", "filter_people": "篩選人物", "filter_places": "篩選地點", - "filters": "篩檢程式", - "find_them_fast": "透過搜尋名稱快速找到他們", + "filters": "篩選器", + "find_them_fast": "透過搜尋姓名快速找到他們", "first": "第一個", "fix_incorrect_match": "修復不相符的", "folder": "資料夾", @@ -1213,16 +1228,16 @@ "folders_feature_description": "透過資料夾檢視瀏覽檔案系統中的相片與影片", "forgot_pin_code_question": "忘記您的 PIN 碼?", "forward": "由新至舊", - "free_up_space": "釋放內存", - "free_up_space_description": "已備份照片和影片已經移到裝置的垃圾桶以釋放內存。伺服器上的存檔依然安全。", - "free_up_space_settings_subtitle": "釋放裝置內存", + "free_up_space": "釋放空間", + "free_up_space_description": "將已備份的相片與影片移至裝置垃圾桶以釋放空間。伺服器上的備份將保持安全。", + "free_up_space_settings_subtitle": "釋放裝置儲存空間", "full_path": "完整路徑:{path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "此功能需要從 Google 載入外部資源才能正常運作。", "general": "一般", "geolocation_instruction_location": "點選具有 GPS 座標的項目以使用其位置,或直接從地圖中選擇地點", "get_help": "取得協助", - "get_people_error": "獲取人員時出錯", + "get_people_error": "取得人物時發生錯誤", "get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連線至 Wi-Fi 網路", "getting_started": "開始使用", "go_back": "上一頁", @@ -1233,15 +1248,15 @@ "grant_permission": "授予權限", "group_albums_by": "分類群組的方式...", "group_country": "按照國家分類", - "group_no": "沒有分類", + "group_no": "不分組", "group_owner": "按擁有者分類", "group_places_by": "分類地點的方式...", "group_year": "按年份分類", "haptic_feedback_switch": "啟用震動回饋", "haptic_feedback_title": "震動回饋", "has_quota": "已設定配額", - "hash_asset": "雜湊媒體", - "hashed_assets": "已雜湊的媒體", + "hash_asset": "雜湊項目", + "hashed_assets": "已雜湊的項目", "hashing": "正在計算雜湊值", "header_settings_add_header_tip": "新增標頭", "header_settings_field_validator_msg": "值不可為空", @@ -1256,42 +1271,42 @@ "hide_password": "隱藏密碼", "hide_person": "隱藏人物", "hide_schema": "隱藏架構", - "hide_text_recognition": "隱藏文字識別", + "hide_text_recognition": "隱藏文字辨識", "hide_unnamed_people": "隱藏未命名的人物", - "home_page_add_to_album_conflicts": "已將 {added} 個媒體新增到相簿 {album}。{failed} 個媒體已在該相簿中。", - "home_page_add_to_album_err_local": "暫時不能將本機媒體新增到相簿,已略過", - "home_page_add_to_album_success": "已在 {album} 相簿中新增 {added} 個媒體。", - "home_page_album_err_partner": "暫時不能無法將親朋好友的媒體新增到相簿,已略過", - "home_page_archive_err_local": "暫時不能封存本機媒體,已略過", - "home_page_archive_err_partner": "無法封存親朋好友的媒體,已略過", + "home_page_add_to_album_conflicts": "已將 {added} 個項目新增至相簿 {album}。{failed} 個項目已在該相簿中。", + "home_page_add_to_album_err_local": "目前無法將本機項目新增至相簿,已略過", + "home_page_add_to_album_success": "已將 {added} 個項目新增至相簿 {album}。", + "home_page_album_err_partner": "目前無法將親友共享項目新增至相簿,已略過", + "home_page_archive_err_local": "目前無法封存本機項目,已略過", + "home_page_archive_err_partner": "無法封存親友共享項目,已略過", "home_page_building_timeline": "正在建立時間軸", - "home_page_delete_err_partner": "無法刪除親朋好友的媒體,已略過", - "home_page_delete_remote_err_local": "刪除遠端媒體的選取中包含本機媒體,已略過", - "home_page_favorite_err_local": "暫不能收藏本機項目,略過", - "home_page_favorite_err_partner": "暫無法收藏親朋好友的項目,略過", - "home_page_first_time_notice": "如果這是您第一次使用本程式,請確保選擇一個要備份的相簿,以將照片與影片加入時間軸", - "home_page_locked_error_local": "無法移動本機檔案至鎖定的資料夾,已略過", - "home_page_locked_error_partner": "無法移動親朋好友分享的媒體至鎖定的資料夾,已略過", - "home_page_share_err_local": "無法透過連結共享本機媒體,已略過", - "home_page_upload_err_limit": "一次最多隻能上傳 30 個媒體,已略過", + "home_page_delete_err_partner": "無法刪除親友共享項目,已略過", + "home_page_delete_remote_err_local": "選取的遠端刪除清單包含本機項目,已略過", + "home_page_favorite_err_local": "暫時無法將本機項目設為收藏,已略過", + "home_page_favorite_err_partner": "暫時無法將親友共享項目設為收藏,已略過", + "home_page_first_time_notice": "如果這是您第一次使用本程式,請確保選擇一個要備份的相簿,以將相片與影片加入時間軸", + "home_page_locked_error_local": "無法將本機項目移動至「已鎖定」資料夾,已略過", + "home_page_locked_error_partner": "無法將親友共享項目移動至「已鎖定」資料夾,已略過", + "home_page_share_err_local": "無法透過連結分享本機項目,已略過", + "home_page_upload_err_limit": "一次最多只能上傳 30 個項目,已略過", "host": "主機", "hour": "小時", "hours": "小時", "id": "ID", "idle": "閒置", - "ignore_icloud_photos": "忽略 iCloud 照片", - "ignore_icloud_photos_description": "儲存在 iCloud 中的照片不會上傳至 Immich 伺服器", + "ignore_icloud_photos": "忽略 iCloud 相片", + "ignore_icloud_photos_description": "儲存在 iCloud 中的相片不會上傳至 Immich 伺服器", "image": "圖片", "image_alt_text_date": "{isVideo, select, true {影片} other {圖片}}拍攝於 {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 一同於 {date} 拍攝", - "image_alt_text_date_2_people": "{person1} 和 {person2} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", - "image_alt_text_date_3_people": "{person1}、{person2} 和 {person3} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", - "image_alt_text_date_4_or_more_people": "{person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_1_person": "{isVideo, select, true {影片} other {相片}}:於 {date} 與 {person1} 一同拍攝", + "image_alt_text_date_2_people": "{isVideo, select, true {影片} other {相片}}:於 {date} 與 {person1} 及 {person2} 一同拍攝", + "image_alt_text_date_3_people": "{isVideo, select, true {影片} other {相片}}:於 {date} 與 {person1}、{person2} 及 {person3} 一同拍攝", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {影片} other {相片}}:於 {date} 與 {person1}、{person2} 及其他 {additionalCount, number} 人一同拍攝", "image_alt_text_date_place": "於 {date} 在 {country} - {city} 拍攝的{isVideo, select, true {影片} other {圖片}}", - "image_alt_text_date_place_1_person": "在 {country} - {city},與 {person1} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", - "image_alt_text_date_place_2_people": "在 {country} - {city} 與 {person1} 和 {person2} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", - "image_alt_text_date_place_3_people": "在 {country} - {city} 與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", - "image_alt_text_date_place_4_or_more_people": "在 {country} - {city} 與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝的{isVideo, select, true {影片} other {圖片}}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {影片} other {相片}}:於 {date} 在 {country}{city} 與 {person1} 一同拍攝", + "image_alt_text_date_place_2_people": "{isVideo, select, true {影片} other {相片}}:於 {date} 在 {country}{city} 與 {person1} 及 {person2} 一同拍攝", + "image_alt_text_date_place_3_people": "{isVideo, select, true {影片} other {相片}}:於 {date} 在 {country}{city} 與 {person1}、{person2} 及 {person3} 一同拍攝", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {影片} other {相片}}:於 {date} 在 {country}{city} 與 {person1}、{person2} 及其他 {additionalCount, number} 人一同拍攝", "image_saved_successfully": "已儲存圖片", "image_viewer_page_state_provider_download_started": "下載已啟動", "image_viewer_page_state_provider_download_success": "下載成功", @@ -1306,7 +1321,7 @@ "in_year_selector": "在", "include_archived": "包含已封存", "include_shared_albums": "包含共享相簿", - "include_shared_partner_assets": "包括共享親朋好友的媒體", + "include_shared_partner_assets": "包含親友共享項目", "individual_share": "個別分享", "individual_shares": "個別分享", "info": "資訊", @@ -1318,7 +1333,7 @@ }, "invalid_date": "無效的日期", "invalid_date_format": "無效的日期格式", - "invite_people": "邀請人員", + "invite_people": "邀請成員", "invite_to_album": "邀請至相簿", "ios_debug_info_fetch_ran_at": "抓取已於 {dateTime} 執行", "ios_debug_info_last_sync_at": "上次同步於 {dateTime}", @@ -1332,8 +1347,9 @@ "json_error": "JSON錯誤", "keep": "保留", "keep_albums": "保留相簿", + "keep_albums_count": "保留{count} {count, plural, one {個相簿} other {個相簿}}", "keep_all": "全部保留", - "keep_description": "選擇釋放空間時,保留在裝置上的相片", + "keep_description": "選擇執行釋放空間時要保留在裝置上的項目。", "keep_favorites": "保留最愛的相片", "keep_on_device": "保留在裝置上", "keep_on_device_hint": "選擇保留在裝置上的相片", @@ -1357,14 +1373,14 @@ "lens_model": "鏡頭型號", "let_others_respond": "允許他人回覆", "level": "等級", - "library": "相簿", - "library_add_folder": "添加資料夾", + "library": "媒體庫", + "library_add_folder": "新增資料夾", "library_edit_folder": "編輯資料夾", - "library_options": "資料庫選項", + "library_options": "媒體庫選項", "library_page_device_albums": "裝置上的相簿", "library_page_new_album": "新增相簿", "library_page_sort_asset_count": "項目數量", - "library_page_sort_created": "新增日期", + "library_page_sort_created": "建立日期", "library_page_sort_last_modified": "上次修改", "library_page_sort_title": "相簿標題", "licenses": "授權", @@ -1374,7 +1390,7 @@ "link_motion_video": "連結動態影片", "link_to_oauth": "連結 OAuth", "linked_oauth_account": "已連結 OAuth 帳號", - "list": "列表", + "list": "清單", "loading": "載入中", "loading_search_results_failed": "載入搜尋結果失敗", "local": "本機", @@ -1393,8 +1409,8 @@ "location_picker_longitude_error": "輸入有效的經度值", "location_picker_longitude_hint": "請在此處輸入您的經度值", "lock": "鎖定", - "locked_folder": "鎖定的資料夾", - "log_detail_title": "日誌詳細資訊", + "locked_folder": "已鎖定資料夾", + "log_detail_title": "紀錄詳細資訊", "log_out": "登出", "log_out_all_devices": "登出所有裝置", "logged_in_as": "以{user}身分登入", @@ -1410,12 +1426,12 @@ "login_form_err_http": "請註明 http:// 或 https://", "login_form_err_invalid_email": "電子郵件地址無效", "login_form_err_invalid_url": "無效的 URL", - "login_form_err_leading_whitespace": "帶有前導空格", - "login_form_err_trailing_whitespace": "帶有尾隨空格", - "login_form_failed_get_oauth_server_config": "使用 OAuth 登入時錯誤,請檢查伺服器位址", + "login_form_err_leading_whitespace": "開頭包含空白字元", + "login_form_err_trailing_whitespace": "結尾包含空白字元", + "login_form_failed_get_oauth_server_config": "使用 OAuth 登入時發生錯誤,請檢查伺服器網址", "login_form_failed_get_oauth_server_disable": "OAuth 功能在此伺服器上無法使用", "login_form_failed_login": "登入失敗,請檢查伺服器位址、電子郵件地址與密碼", - "login_form_handshake_exception": "與伺服器通訊時出現握手異常。若使用自簽名憑證,請在設定中啟用自簽名憑證支援。", + "login_form_handshake_exception": "與伺服器通訊時出現交握異常。若使用自簽憑證,請在設定中啟用自簽憑證支援。", "login_form_password_hint": "密碼", "login_form_save_login": "保持登入", "login_form_server_empty": "請輸入伺服器網址。", @@ -1425,56 +1441,59 @@ "login_password_changed_success": "密碼更新成功", "logout_all_device_confirmation": "您確定要登出所有裝置嗎?", "logout_this_device_confirmation": "要登出這臺裝置嗎?", - "logs": "日誌", + "logs": "紀錄", "longitude": "經度", "look": "樣貌", "loop_videos": "重播影片", "loop_videos_description": "啟用後,影片結束會自動重播。", - "main_branch_warning": "您現在使用的是開發版本;我們強烈您建議使用正式發行版!", + "main_branch_warning": "您正使用開發版本;強烈建議使用正式版本!", "main_menu": "主選單", - "maintenance_action_restore": "復原資料庫", - "maintenance_description": "Immich已進入維護模式。", + "maintenance_action_restore": "還原資料庫", + "maintenance_description": "Immich 已進入 維護模式。", "maintenance_end": "結束維護模式", "maintenance_end_error": "未能結束維護模式。", - "maintenance_logged_in_as": "當前以{user}身份登入", - "maintenance_restore_from_backup": "從備份復原", - "maintenance_restore_library": "復原你的相簿", - "maintenance_restore_library_confirm": "確認是否正確,將繼續從備份復原!", - "maintenance_restore_library_description": "正在復原資料庫", - "maintenance_restore_library_folder_has_files": "{folder}有{count}個資料夾", - "maintenance_restore_library_folder_no_files": "{folder}有缺失的檔案!", - "maintenance_restore_library_folder_pass": "可以讀寫", + "maintenance_logged_in_as": "目前以 {user} 身分登入", + "maintenance_restore_from_backup": "從備份還原", + "maintenance_restore_library": "還原您的媒體庫", + "maintenance_restore_library_confirm": "確認是否正確,將繼續還原備份!", + "maintenance_restore_library_description": "正在還原資料庫", + "maintenance_restore_library_folder_has_files": "{folder} 含有 {count} 個資料夾", + "maintenance_restore_library_folder_no_files": "{folder} 缺少檔案!", + "maintenance_restore_library_folder_pass": "可讀取與寫入", "maintenance_restore_library_folder_read_fail": "無法讀取", "maintenance_restore_library_folder_write_fail": "無法寫入", - "maintenance_restore_library_hint_missing_files": "可能遺失重要檔案", + "maintenance_restore_library_hint_missing_files": "您可能遺失了重要檔案", "maintenance_restore_library_hint_regenerate_later": "之後可以在設定重新產生", + "maintenance_restore_library_hint_storage_template_missing_files": "正在使用儲存範本?您可能遺失了部分檔案", + "maintenance_restore_library_loading": "正在載入完整性檢查與啟發式分析…", "maintenance_task_backup": "正在建立現有資料庫的備份…", - "maintenance_task_restore": "正在從選擇的備份復原…", - "maintenance_task_rollback": "復原失敗,恢復到之前的儲存…", + "maintenance_task_migrations": "正在執行資料庫遷移…", + "maintenance_task_restore": "正在從選取的備份進行還原…", + "maintenance_task_rollback": "還原失敗,正在回溯至還原點…", "maintenance_title": "暫時不可用", "make": "製造商", "manage_geolocation": "管理位置", - "manage_media_access_rationale": "正確處理將資產移至垃圾桶並將其從垃圾桶中恢復需要此許可。", + "manage_media_access_rationale": "需要此權限才能處理項目移至垃圾桶與還原的操作。", "manage_media_access_settings": "打開設定", - "manage_media_access_subtitle": "允許Immich應用程序管理和移動媒體檔案。", - "manage_media_access_title": "媒體管理訪問", - "manage_shared_links": "管理共享連結", - "manage_sharing_with_partners": "管理與親朋好友的分享", + "manage_media_access_subtitle": "允許 Immich App 管理與移動媒體檔案。", + "manage_media_access_title": "媒體管理存取權限", + "manage_shared_links": "管理分享連結", + "manage_sharing_with_partners": "管理親友共享設定", "manage_the_app_settings": "管理應用程式設定", "manage_your_account": "管理您的帳號", "manage_your_api_keys": "管理您的 API 金鑰", "manage_your_devices": "管理已登入的裝置", "manage_your_oauth_connection": "管理您的 OAuth 連結", "map": "地圖", - "map_assets_in_bounds": "{count, plural, one {# 張照片} other {# 張照片}}", + "map_assets_in_bounds": "{count, plural, one {# 張相片} other {# 張相片}}", "map_cannot_get_user_location": "無法取得使用者位置", "map_location_dialog_yes": "確定", "map_location_picker_page_use_location": "使用此位置", - "map_location_service_disabled_content": "需要啟用定位服務才能顯示目前位置相關的項目。要現在啟用嗎?", + "map_location_service_disabled_content": "需要啟用定位服務才能顯示您目前位置相關的項目。要現在啟用嗎?", "map_location_service_disabled_title": "定位服務已停用", "map_marker_for_images": "在 {city}、{country} 拍攝影像的地圖示記", "map_marker_with_image": "帶有影像的地圖示記", - "map_no_location_permission_content": "需要位置權限才能顯示與目前位置。要現在就授予位置權限嗎?", + "map_no_location_permission_content": "需要位置權限才能顯示與您目前位置相關的項目。要現在就授予位置權限嗎?", "map_no_location_permission_title": "沒有位置權限", "map_settings": "地圖設定", "map_settings_dark_mode": "深色模式", @@ -1484,7 +1503,7 @@ "map_settings_date_range_option_years": "{years} 年前", "map_settings_dialog_title": "地圖設定", "map_settings_include_show_archived": "包括已封存項目", - "map_settings_include_show_partners": "包含親朋好友", + "map_settings_include_show_partners": "包含親友", "map_settings_only_show_favorites": "僅顯示收藏的項目", "map_settings_theme_settings": "地圖主題", "map_zoom_to_see_photos": "縮小以檢視項目", @@ -1492,7 +1511,7 @@ "mark_as_read": "標記為已讀", "marked_all_as_read": "已全部標記為已讀", "matches": "相符", - "matching_assets": "匹配資產", + "matching_assets": "符合的項目", "media_type": "媒體類型", "memories": "回憶", "memories_all_caught_up": "已全部看完", @@ -1508,15 +1527,15 @@ "merge_people_limit": "一次最多隻能合併 5 張臉孔", "merge_people_prompt": "您要合併這些人物嗎?此操作無法撤銷。", "merge_people_successfully": "成功合併人物", - "merged_people_count": "合併了 {count, plural, one {# 位人士} other {# 位人士}}", + "merged_people_count": "已合併 {count, plural, other {# 位人物}}", "minimize": "最小化", "minute": "分", "minutes": "分鐘", "mirror_horizontal": "水平", "mirror_vertical": "垂直", "missing": "排入未處理", - "mobile_app": "移動應用程序", - "mobile_app_download_onboarding_note": "使用以下選項下載配套移動應用程序", + "mobile_app": "行動應用程式", + "mobile_app_download_onboarding_note": "請使用以下選項下載隨附的行動應用程式", "model": "型號", "month": "月", "monthly_title_text_date_format": "y MMMM", @@ -1526,26 +1545,26 @@ "move_off_locked_folder": "移出鎖定的資料夾", "move_to": "移動到", "move_to_device_trash": "移動到裝置的垃圾桶", - "move_to_lock_folder_action_prompt": "{count} 已新增至鎖定的資料夾中", - "move_to_locked_folder": "移至鎖定的資料夾", - "move_to_locked_folder_confirmation": "這些照片和影片將從所有相簿中移除,並僅可從鎖定的資料夾檢視", + "move_to_lock_folder_action_prompt": "已將 {count} 個項目新增至「已鎖定」資料夾", + "move_to_locked_folder": "移至「已鎖定」資料夾", + "move_to_locked_folder_confirmation": "這些相片與影片將從所有相簿中移除,且僅能從「已鎖定」資料夾中檢視", "move_up": "向上移動", "moved_to_archive": "已封存 {count, plural, one {# 個項目} other {# 個項目}}", "moved_to_library": "已移動 {count, plural, one {# 個項目} other {# 個項目}} 至相簿", "moved_to_trash": "已丟進垃圾桶", - "multiselect_grid_edit_date_time_err_read_only": "無法編輯唯讀項目的日期,略過", - "multiselect_grid_edit_gps_err_read_only": "無法編輯唯讀項目的位置資訊,略過", + "multiselect_grid_edit_date_time_err_read_only": "唯讀項目的日期無法編輯,已略過", + "multiselect_grid_edit_gps_err_read_only": "唯讀項目的位置資訊無法編輯,已略過", "mute_memories": "靜音回憶", "my_albums": "我的相簿", "name": "名稱", "name_or_nickname": "名稱或暱稱", "name_required": "名稱是必填項", "navigate": "導航", - "navigate_to_time": "導航到時間", - "network_requirement_photos_upload": "使用行動網路流量備份照片", + "navigate_to_time": "跳轉至指定時間", + "network_requirement_photos_upload": "使用行動網路流量備份相片", "network_requirement_videos_upload": "使用行動網路流量備份影片", "network_requirements": "網路要求", - "network_requirements_updated": "網路需求已變更,現重設備份佇列", + "network_requirements_updated": "網路需求已變更,正在重設備份佇列", "networking_settings": "網路", "networking_subtitle": "管理伺服器端點設定", "never": "永不失效", @@ -1555,7 +1574,7 @@ "new_password": "新密碼", "new_person": "新的人物", "new_pin_code": "新 PIN 碼", - "new_pin_code_subtitle": "這是您第一次存取鎖定的資料夾。建立 PIN 碼以安全存取此頁面", + "new_pin_code_subtitle": "這是您第一次存取「已鎖定」資料夾。請建立 PIN 碼以安全存取此頁面", "new_timeline": "新時間軸", "new_update": "新更新", "new_user_created": "已建立新使用者", @@ -1564,43 +1583,42 @@ "next": "下一步", "next_memory": "下一張回憶", "no": "否", - "no_actions_added": "尚未添加任何操作", + "no_actions_added": "尚未新增任何動作", "no_albums_found": "無相簿", - "no_albums_message": "建立相簿來整理照片和影片", + "no_albums_message": "建立相簿來整理相片和影片", "no_albums_with_name_yet": "看來還沒有這個名字的相簿。", "no_albums_yet": "看來您還沒有任何相簿。", - "no_archived_assets_message": "將照片和影片封存,就不會顯示在「照片」中", - "no_assets_message": "按這裡上傳您的第一張照片", + "no_archived_assets_message": "將相片與影片封存後,就不會顯示在「相片」視圖中", + "no_assets_message": "按這裡上傳您的第一張相片", "no_assets_to_show": "無項目展示", "no_cast_devices_found": "找不到 Google Cast 裝置", - "no_checksum_local": "沒有可用的校驗和 - 無法取得本機資產", - "no_checksum_remote": "沒有可用的校驗和 - 無法取得遠端資產", - "no_configuration_needed": "無需配寘", - "no_devices": "無授權設備", - "no_duplicates_found": "沒發現重複項目。", + "no_checksum_local": "無可用校驗碼 - 無法取得本機項目", + "no_checksum_remote": "無可用校驗碼 - 無法取得雲端項目", + "no_configuration_needed": "無需設定", + "no_devices": "無授權裝置", + "no_duplicates_found": "未發現任何重複項目。", "no_exif_info_available": "沒有可用的 Exif 資訊", - "no_explore_results_message": "上傳更多照片以利探索。", + "no_explore_results_message": "上傳更多相片來探索您的珍藏。", "no_favorites_message": "加入收藏,加速尋找影像", - "no_filters_added": "尚未添加篩檢程式", - "no_libraries_message": "建立外部媒體庫以檢視您的照片和影片", - "no_local_assets_found": "未找到具有此校驗和的本機資產", + "no_filters_added": "尚未新增任何篩選器", + "no_libraries_message": "建立外部媒體庫以檢視您的相片和影片", + "no_local_assets_found": "找不到具有此校驗碼的本機項目", "no_location_set": "未設定位置", - "no_locked_photos_message": "鎖定的資料夾中的照片和影片會被隱藏,當您瀏覽或搜尋相簿時不會顯示。", + "no_locked_photos_message": "「已鎖定」資料夾中的相片與影片會被隱藏,且不會出現在瀏覽或搜尋結果中。", "no_name": "無名", "no_notifications": "沒有通知", "no_people_found": "找不到符合的人物", "no_places": "沒有地點", - "no_remote_assets_found": "未找到具有此校驗和的遠端資產", + "no_remote_assets_found": "找不到具有此校驗碼的雲端項目", "no_results": "沒有結果", "no_results_description": "試試同義詞或更通用的關鍵字吧", - "no_shared_albums_message": "建立相簿分享照片和影片", + "no_shared_albums_message": "建立共享相簿以分享相片與影片", "no_uploads_in_progress": "沒有正在上傳的項目", "none": "無", "not_allowed": "不允許", "not_available": "不適用", "not_in_any_album": "不在任何相簿中", - "not_selected": "未選擇", - "note_apply_storage_label_to_previously_uploaded assets": "*註:執行套用儲存標籤前先上傳項目", + "not_selected": "未選取", "notes": "提示", "nothing_here_yet": "暫無訊息", "notification_permission_dialog_content": "開啟通知,請前往「設定」,並選擇「允許」。", @@ -1611,8 +1629,8 @@ "notifications": "通知", "notifications_setting_description": "管理通知", "oauth": "OAuth", - "obtainium_configurator": "Obtainium配寘器", - "obtainium_configurator_instructions": "使用Obtainium直接從Immich GitHub的版本安裝和更新Android應用程序。 創建一個API金鑰並選擇一個變體來創建您的Obtainium配寘連結", + "obtainium_configurator": "Obtainium 設定器", + "obtainium_configurator_instructions": "使用 Obtainium 直接從 Immich GitHub 的發行版本安裝並更新 Android App。建立 API 金鑰並選取版本類型以產生您的 Obtainium 設定連結", "ocr": "OCR", "official_immich_resources": "官方 Immich 資源", "offline": "離線", @@ -1622,21 +1640,22 @@ "on_this_device": "在此裝置", "onboarding": "入門指南", "onboarding_locale_description": "選擇您想要顯示的語言。設定完成之後生效。", - "onboarding_privacy_description": "以下(可選)功能仰賴外部服務,可隨時在設定中停用。", + "onboarding_privacy_description": "以下選用功能仰賴外部服務,您可以隨時在設定中將其停用。", "onboarding_server_welcome_description": "讓我們為您的系統進行一些基本設定。", - "onboarding_theme_description": "幫執行個體選色彩主題。之後也可以在設定中變更。", + "onboarding_theme_description": "請為您的執行個體選擇色彩主題。您稍後仍可在設定中變更。", "onboarding_user_welcome_description": "讓我們開始吧!", "onboarding_welcome_user": "歡迎,{user}", "online": "線上", "only_favorites": "僅顯示己收藏", "open": "開啟", + "open_calendar": "打開日曆", "open_in_map_view": "開啟地圖檢視", "open_in_openstreetmap": "用 OpenStreetMap 開啟", "open_the_search_filters": "開啟搜尋篩選器", "options": "選項", "or": "或", "organize_into_albums": "整理成相簿", - "organize_into_albums_description": "使用目前同步設定將現有照片放入相簿", + "organize_into_albums_description": "使用目前同步設定將現有相片放入相簿", "organize_your_library": "整理您的相簿", "original": "原圖", "other": "其他", @@ -1644,22 +1663,22 @@ "other_entities": "其他項目", "other_variables": "其他變數", "owned": "我的", - "owner": "所有者", + "owner": "擁有者", "page": "頁", - "partner": "親朋好友", - "partner_can_access": "{partner} 可以存取", - "partner_can_access_assets": "除了已封存和已刪除之外,您所有的照片和影片", - "partner_can_access_location": "您照片拍攝的位置", - "partner_list_user_photos": "{user} 的照片", - "partner_list_view_all": "展示全部", - "partner_page_empty_message": "您的照片尚未與任何親朋好友共享。", - "partner_page_no_more_users": "無需新增更多使用者", - "partner_page_partner_add_failed": "新增親朋好友失敗", - "partner_page_select_partner": "選擇親朋好友", + "partner": "親友", + "partner_can_access": "{partner} 可存取", + "partner_can_access_assets": "除了「已封存」與「已刪除」外,您所有的相片與影片", + "partner_can_access_location": "相片的拍攝位置", + "partner_list_user_photos": "{user} 的相片", + "partner_list_view_all": "查看全部", + "partner_page_empty_message": "您的相片尚未與任何親友共享。", + "partner_page_no_more_users": "已無可新增的使用者", + "partner_page_partner_add_failed": "親友新增失敗", + "partner_page_select_partner": "選擇親友", "partner_page_shared_to_title": "共享給", - "partner_page_stop_sharing_content": "{partner} 將無法再存取您的照片。", - "partner_sharing": "親朋好友分享", - "partners": "親朋好友", + "partner_page_stop_sharing_content": "{partner} 將無法再存取您的相片。", + "partner_sharing": "親友共享", + "partners": "親友", "password": "密碼", "password_does_not_match": "密碼不相符", "password_required": "需要密碼", @@ -1676,9 +1695,9 @@ "paused": "已暫停", "pending": "待處理", "people": "人物", - "people_edits_count": "編輯了 {count, plural, one {# 位人士} other {# 位人士}}", - "people_feature_description": "以人物分類瀏覽照片和影片", - "people_selected": "{count, plural, one {# 個人已選擇} other {# 個人已選擇}}", + "people_edits_count": "已編輯 {count, plural, one {# 位人物} other {# 位人物}}", + "people_feature_description": "以人物分類瀏覽相片和影片", + "people_selected": "{count, plural, one {已選取 # 位人物} other {已選取 # 位人物}}", "people_sidebar_description": "在側邊欄顯示「人物」的連結", "permanent_deletion_warning": "永久刪除警告", "permanent_deletion_warning_setting_description": "在永久刪除檔案時顯示警告", @@ -1696,40 +1715,40 @@ "permission_onboarding_permission_denied": "如要繼續,請允許 Immich 存取相片和影片權限。", "permission_onboarding_permission_granted": "已允許!一切就緒。", "permission_onboarding_permission_limited": "如要繼續,請允許 Immich 備份和管理您的相簿收藏,在設定中授予相片和影片權限。", - "permission_onboarding_request": "Immich 需要權限才能檢視您的相片和短片。", + "permission_onboarding_request": "Immich 需要權限才能檢視您的相片與影片。", "person": "人物", "person_age_months": "{months, plural, one {# 個月} other {# 個月}}", "person_age_year_months": "1 年 {months, plural, one {# 個月} other {# 個月}}", "person_age_years": "{years, plural, other {# 歲}}", - "person_birthdate": "生於 {date}", + "person_birthdate": "出生於 {date}", "person_hidden": "{name}{hidden, select, true {(隱藏)} other {}}", - "person_recognized": "被認可的人", - "person_selected": "已選擇的人", - "photo_shared_all_users": "看來您與所有使用者分享了照片,或沒有其他使用者可供分享。", - "photos": "照片", - "photos_and_videos": "照片及影片", - "photos_count": "{count, plural, other {{count, number} 張照片}}", - "photos_from_previous_years": "往年的照片", - "photos_only": "只允許照片", + "person_recognized": "已辨識人物", + "person_selected": "已選取人物", + "photo_shared_all_users": "看來您與所有使用者分享了相片,或沒有其他使用者可供分享。", + "photos": "相片", + "photos_and_videos": "相片及影片", + "photos_count": "{count, plural, other {{count, number} 張相片}}", + "photos_from_previous_years": "往年的相片", + "photos_only": "僅限相片", "pick_a_location": "選擇位置", "pick_custom_range": "自定義範圍", "pick_date_range": "選擇日期範圍", - "pin_code_changed_successfully": "變更 PIN 碼成功", - "pin_code_reset_successfully": "重設 PIN 碼成功", - "pin_code_setup_successfully": "設定 PIN 碼成功", + "pin_code_changed_successfully": "PIN 碼變更成功", + "pin_code_reset_successfully": "PIN 碼重設成功", + "pin_code_setup_successfully": "PIN 碼設定成功", "pin_verification": "PIN 碼驗證", "place": "地點", "places": "地點", "places_count": "{count, plural, one {{count, number} 個地點} other {{count, number} 個地點}}", "play": "播放", "play_memories": "播放回憶", - "play_motion_photo": "播放動態照片", + "play_motion_photo": "播放動態相片", "play_or_pause_video": "播放或暫停影片", - "play_original_video": "播放原始視頻", - "play_original_video_setting_description": "更喜歡播放原始視頻,而不是轉碼視頻。 如果原始資源不相容,則可能無法正確播放。", - "play_transcoded_video": "播放轉碼視頻", + "play_original_video": "播放原始影片", + "play_original_video_setting_description": "優先播放原始影片而非轉碼後的影片。若原始項目不相容,可能無法正常播放。", + "play_transcoded_video": "播放轉碼影片", "please_auth_to_access": "請進行身份驗證才能存取", - "port": "埠口", + "port": "連接埠", "preferences_settings_subtitle": "管理應用程式偏好設定", "preferences_settings_title": "偏好設定", "preparing": "準備", @@ -1739,72 +1758,72 @@ "previous_memory": "上一張回憶", "previous_or_next_day": "前一天/後一天", "previous_or_next_month": "上一個月/下一個月", - "previous_or_next_photo": "上一張照片/下一張照片", + "previous_or_next_photo": "上一張相片/下一張相片", "previous_or_next_year": "上一年/下一年", "primary": "首要", "privacy": "隱私", "profile": "帳戶設定", - "profile_drawer_app_logs": "日誌", - "profile_drawer_client_server_up_to_date": "用戶端與伺服器端都是最新的", + "profile_drawer_app_logs": "紀錄", + "profile_drawer_client_server_up_to_date": "用戶端與伺服器版本皆為最新", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "唯讀模式已開啟。請長按使用者頭像圖示以結束。", + "profile_drawer_readonly_mode": "唯讀模式已啟用。長按使用者個人圖示即可退出。", "profile_image_of_user": "{user} 的個人資料圖片", "profile_picture_set": "已設定個人資料圖片。", "public_album": "公開相簿", "public_share": "公開分享", - "purchase_account_info": "擁護者", - "purchase_activated_subtitle": "感謝您對 Immich 及開源軟體的支援", + "purchase_account_info": "支持者", + "purchase_activated_subtitle": "感謝您對 Immich 及開源軟體的支持", "purchase_activated_time": "於 {date} 啟用", - "purchase_activated_title": "金鑰成功啟用了", + "purchase_activated_title": "金鑰已成功啟用", "purchase_button_activate": "啟用", - "purchase_button_buy": "購置", - "purchase_button_buy_immich": "購置 Immich", + "purchase_button_buy": "購買", + "purchase_button_buy_immich": "購買 Immich", "purchase_button_never_show_again": "不再顯示", - "purchase_button_reminder": "過 30 天再提醒我", + "purchase_button_reminder": "30 天後提醒我", "purchase_button_remove_key": "移除金鑰", - "purchase_button_select": "選這個", + "purchase_button_select": "選擇", "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件,取得正確的產品金鑰!", "purchase_individual_description_1": "針對個人", - "purchase_individual_description_2": "擁護者狀態", + "purchase_individual_description_2": "支持者狀態", "purchase_individual_title": "個人", - "purchase_input_suggestion": "有產品金鑰嗎?請在下面輸入金鑰", - "purchase_license_subtitle": "購置 Immich 來支援軟體開發", - "purchase_lifetime_description": "終身購置", - "purchase_option_title": "購置選項", - "purchase_panel_info_1": "開發 Immich 可不是件容易的事,花了我們不少功夫。好在有一群全職工程師在背後默默努力,為的就是把它做到最好。我們的目標很簡單:讓開放原始碼軟體和正當的商業模式能成為開發者的長期飯碗,同時打造出重視隱私的生態系統,讓大家有個不被限制的雲端服務新選擇。", - "purchase_panel_info_2": "我們承諾不設付費牆,所以購置 Immich 並不會讓您獲得額外的功能。我們仰賴使用者們的支援來開發 Immich。", - "purchase_panel_title": "支援這項專案", + "purchase_input_suggestion": "已有產品金鑰?請在下方輸入", + "purchase_license_subtitle": "購買 Immich 以支持軟體的持續開發", + "purchase_lifetime_description": "終身授權", + "purchase_option_title": "購買選項", + "purchase_panel_info_1": "開發 Immich 需要投入大量時間與精力,我們有全職工程師致力於將其打造得盡善盡美。我們的使命是讓開源軟體與合乎道德的商業模式成為開發者永續的收入來源,並建立一個重視隱私的生態系統,提供取代剝削性雲端服務的真實選擇。", + "purchase_panel_info_2": "我們承諾不設付費牆,所以購買 Immich 並不會讓您獲得額外的功能。我們仰賴像您這樣的使用者來支持 Immich 的持續開發。", + "purchase_panel_title": "支持此專案", "purchase_per_server": "每臺伺服器", "purchase_per_user": "每位使用者", "purchase_remove_product_key": "移除產品金鑰", "purchase_remove_product_key_prompt": "確定要移除產品金鑰嗎?", "purchase_remove_server_product_key": "移除伺服器產品金鑰", "purchase_remove_server_product_key_prompt": "確定要移除伺服器產品金鑰嗎?", - "purchase_server_description_1": "給整臺伺服器", - "purchase_server_description_2": "擁護者狀態", + "purchase_server_description_1": "適用於全伺服器", + "purchase_server_description_2": "支持者狀態", "purchase_server_title": "伺服器", - "purchase_settings_server_activated": "伺服器產品金鑰是由管理者管理的", - "query_asset_id": "査詢資產 ID", + "purchase_settings_server_activated": "伺服器產品金鑰由管理員管理", + "query_asset_id": "查詢項目 ID", "queue_status": "處理中 {count}/{total}", - "rate_asset": "資產評星", + "rate_asset": "項目評分", "rating": "評星", "rating_clear": "清除評等", "rating_count": "{count, plural, other {# 星}}", "rating_description": "在資訊面板中顯示 EXIF 評等", "rating_set": "已設定為{rating, plural, one {# 星} other {# 星}}", "reaction_options": "反應選項", - "read_changelog": "閱覽變更日誌", - "readonly_mode_disabled": "唯讀模式已關閉", + "read_changelog": "閱覽更新紀錄", + "readonly_mode_disabled": "唯讀模式已停用", "readonly_mode_enabled": "唯讀模式已開啟", "ready_for_upload": "已準備好上傳", - "reassign": "重新指定", - "reassigned_assets_to_existing_person": "已將 {count, plural, other {# 個檔案}}重新指定給{name, select, null {現有的人} other {{name}}}", - "reassigned_assets_to_new_person": "已將 {count, plural, other {# 個檔案}}重新指定給一位新人物", - "reassing_hint": "將選定的檔案分配給己存在的人物", + "reassign": "重新指派", + "reassigned_assets_to_existing_person": "已將 {count, plural, other {# 個項目}} 重新指派給 {name, select, null {現有人物} other {{name}}}", + "reassigned_assets_to_new_person": "已將 {count, plural, other {# 個項目}} 重新指派給新的人物", + "reassing_hint": "將選取的項目指派給現有人物", "recent": "最近", - "recent-albums": "最近相簿", + "recent_albums": "最近相簿", "recent_searches": "最近搜尋項目", - "recently_added": "近期新增", + "recently_added": "最近新增", "recently_added_page_title": "最近新增", "recently_taken": "最近拍攝", "recently_taken_page_title": "最近拍攝", @@ -1814,7 +1833,7 @@ "refresh_metadata": "重新整理中繼資料", "refresh_thumbnails": "重新整理縮圖", "refreshed": "重新整理完畢", - "refreshes_every_file": "重新讀取現有的所有檔案和新檔案", + "refreshes_every_file": "重新讀取所有現有與新增檔案", "refreshing_encoded_video": "正在重新整理已編碼的影片", "refreshing_faces": "重整面部資料中", "refreshing_metadata": "正在重新整理中繼資料", @@ -1824,16 +1843,16 @@ "remote_media_summary": "遠端媒體摘要", "remove": "移除", "remove_assets_album_confirmation": "確定要從相簿中移除 {count, plural, other {# 個檔案}}嗎?", - "remove_assets_shared_link_confirmation": "確定刪除共享連結中{count, plural, other {# 個項目}}嗎?", + "remove_assets_shared_link_confirmation": "確定要從此分享連結中移除 {count, plural, other {# 個項目}} 嗎?", "remove_assets_title": "移除檔案?", "remove_custom_date_range": "移除自訂日期範圍", "remove_deleted_assets": "移除離線檔案", "remove_from_album": "從相簿中移除", "remove_from_album_action_prompt": "已從相簿中移除了 {count} 個項目", "remove_from_favorites": "從收藏中移除", - "remove_from_lock_folder_action_prompt": "已從鎖定的資料夾中移除了 {count} 個項目", - "remove_from_locked_folder": "從鎖定的資料夾中移除", - "remove_from_locked_folder_confirmation": "您確定要將這些照片和影片移出鎖定的資料夾嗎?這些內容將會顯示在您的相簿中。", + "remove_from_lock_folder_action_prompt": "已從「已鎖定」資料夾中移除 {count} 個項目", + "remove_from_locked_folder": "從「已鎖定」資料夾中移除", + "remove_from_locked_folder_confirmation": "您確定要將這些相片與影片移出「已鎖定」資料夾嗎?移出後將會出現在您的媒體庫中。", "remove_from_shared_link": "從共享連結中移除", "remove_memory": "移除記憶", "remove_photo_from_memory": "將圖片從此記憶中移除", @@ -1845,11 +1864,11 @@ "removed_from_favorites": "已從收藏中移除", "removed_from_favorites_count": "已移除收藏的 {count, plural, other {# 個項目}}", "removed_memory": "已移除記憶", - "removed_photo_from_memory": "已從記憶中移除照片", + "removed_photo_from_memory": "已從記憶中移除相片", "removed_tagged_assets": "已移除 {count, plural, one {# 個檔案} other {# 個檔案}}的標籤", "rename": "改名", "repair": "糾正", - "repair_no_results_message": "未被追蹤及未處理的檔案會顯示在這裡", + "repair_no_results_message": "未被追蹤及遺失的檔案會顯示在這裡", "replace_with_upload": "用上傳的檔案取代", "repository": "儲存庫", "require_password": "需要密碼", @@ -1859,19 +1878,19 @@ "reset_password": "重設密碼", "reset_people_visibility": "重設人物可見性", "reset_pin_code": "重設 PIN 碼", - "reset_pin_code_description": "若忘記了 PIN 碼,閣下可要求系統伺服器管理員為您重設", - "reset_pin_code_success": "閣下已成功重設 PIN 碼", + "reset_pin_code_description": "若忘記 PIN 碼,您可以聯絡伺服器管理員進行重設", + "reset_pin_code_success": "PIN 碼已成功重設", "reset_pin_code_with_password": "您可隨時使用您的密碼來重設 PIN 碼", "reset_sqlite": "重設 SQLite 資料庫", - "reset_sqlite_confirmation": "確定要重設 SQLite 資料庫嗎?閣下需登出並重新登入才能重新同步資料", + "reset_sqlite_confirmation": "確定要重設 SQLite 資料庫嗎?您需要登出並重新登入才能重新同步資料", "reset_sqlite_success": "已成功重設 SQLite 資料庫", - "reset_to_default": "重設回預設", - "resolution": "分辯率", + "reset_to_default": "重設為預設值", + "resolution": "解析度", "resolve_duplicates": "解決重複項", "resolved_all_duplicates": "已解決所有重複項目", "restore": "還原", "restore_all": "全部還原", - "restore_trash_action_prompt": "已從垃圾桶復原了 {count} 個項目", + "restore_trash_action_prompt": "已從垃圾桶還原 {count} 個項目", "restore_user": "還原使用者", "restored_asset": "已還原檔案", "resume": "繼續", @@ -1902,9 +1921,9 @@ "search_by_context": "以情境搜尋", "search_by_description": "以描述搜尋", "search_by_description_example": "在沙壩的健行之日", - "search_by_filename": "以檔名或副檔名搜尋", + "search_by_filename": "依檔名或副檔名搜尋", "search_by_filename_example": "如 IMG_1234.JPG 或 PNG", - "search_by_ocr": "通過OCR蒐索", + "search_by_ocr": "透過OCR搜尋", "search_by_ocr_example": "拿鐵", "search_camera_lens_model": "蒐索鏡頭型號...", "search_camera_make": "搜尋相機製造商…", @@ -1923,21 +1942,22 @@ "search_filter_location_title": "選擇位置", "search_filter_media_type": "媒體類型", "search_filter_media_type_title": "選擇媒體類型", - "search_filter_ocr": "通過OCR蒐索", + "search_filter_ocr": "透過OCR搜尋", "search_filter_people_title": "選擇人物", + "search_filter_star_rating": "評分", "search_for": "搜尋", - "search_for_existing_person": "搜尋現有的人物", + "search_for_existing_person": "搜尋現有人物", "search_no_more_result": "無更多結果", "search_no_people": "沒有人找到", "search_no_people_named": "沒有名為「{name}」的人物", "search_no_result": "找不到結果,請嘗試其他搜尋字詞或組合", "search_options": "搜尋選項", "search_page_categories": "類別", - "search_page_motion_photos": "動態照片", + "search_page_motion_photos": "動態相片", "search_page_no_objects": "找不到物件資訊", "search_page_no_places": "找不到地點資訊", "search_page_screenshots": "螢幕截圖", - "search_page_search_photos_videos": "搜尋您的照片與影片", + "search_page_search_photos_videos": "搜尋您的相片與影片", "search_page_selfies": "自拍", "search_page_things": "事物", "search_page_view_all_button": "檢視全部", @@ -1954,34 +1974,35 @@ "search_tags": "搜尋標籤...", "search_timezone": "搜尋時區…", "search_type": "搜尋類型", - "search_your_photos": "搜尋照片", + "search_your_photos": "搜尋相片", "searching_locales": "搜尋區域…", "second": "秒", "see_all_people": "檢視所有人物", "select": "選擇", - "select_album": "選擇相册", + "select_album": "選擇相簿", "select_album_cover": "選擇相簿封面", - "select_albums": "選擇相册", + "select_albums": "選擇相簿", "select_all": "選擇全部", "select_all_duplicates": "保留所有重複項", "select_all_in": "選擇在 {group} 中的所有項目", - "select_avatar_color": "選擇個人資料圖片顏色", + "select_avatar_color": "選擇個人圖示顏色", "select_count": "{count, plural, one {選擇 #} other {選擇 #}}", + "select_cutoff_date": "選擇截止日期", "select_face": "選擇臉孔", - "select_featured_photo": "選擇特色照片", + "select_featured_photo": "選取精選相片", "select_from_computer": "從電腦中選取", "select_keep_all": "全部保留", "select_library_owner": "選擇相簿擁有者", "select_new_face": "選擇新臉孔", - "select_people": "選擇人員", - "select_person": "選擇人員", + "select_people": "選擇人物", + "select_person": "選取人物", "select_person_to_tag": "選擇要標記的人物", - "select_photos": "選照片", + "select_photos": "選相片", "select_trash_all": "全部刪除", - "select_user_for_sharing_page_err_album": "新增相簿失敗", - "selected": "已選擇", - "selected_count": "{count, plural, other {選了 # 項}}", - "selected_gps_coordinates": "選定的 GPS 座標", + "select_user_for_sharing_page_err_album": "建立相簿失敗", + "selected": "已選取", + "selected_count": "{count, plural, other {已選取 # 項}}", + "selected_gps_coordinates": "選取的 GPS 座標", "send_message": "傳訊息", "send_welcome_email": "傳送歡迎電子郵件", "server_endpoint": "伺服器端點", @@ -1990,23 +2011,23 @@ "server_offline": "伺服器已離線", "server_online": "伺服器已上線", "server_privacy": "伺服器隱私", - "server_restarting_description": "此頁面將立即重繪。", - "server_restarting_title": "服務器正在重新啟動", + "server_restarting_description": "此頁面將在稍後自動重新整理。", + "server_restarting_title": "伺服器正在重新啟動", "server_stats": "伺服器統計", - "server_update_available": "服務器更新可用", + "server_update_available": "已有可用的伺服器更新", "server_version": "目前版本", "set": "設定", "set_as_album_cover": "設為相簿封面", - "set_as_featured_photo": "設為特色照片", + "set_as_featured_photo": "設為精選相片", "set_as_profile_picture": "設為個人資料圖片", "set_date_of_birth": "設定出生日期", "set_profile_picture": "設定個人資料圖片", "set_slideshow_to_fullscreen": "以全螢幕放映幻燈片", "set_stack_primary_asset": "設定堆疊的首要項目", - "setting_image_viewer_help": "詳細資訊檢視器首先載入小縮圖,然後載入中等大小的預覽圖(若啟用),最後載入原始圖片。", - "setting_image_viewer_original_subtitle": "啟用以載入原圖,停用以減少資料使用量(包括網路和裝置快取)。", + "setting_image_viewer_help": "詳細資訊檢視器會依序載入小型縮圖、中等尺寸預覽圖(若啟用),最後載入原始相片。", + "setting_image_viewer_original_subtitle": "啟用以載入原始全解析度圖片(檔案較大!)。停用以減少流量使用(包括網路傳輸與裝置快取)。", "setting_image_viewer_original_title": "載入原圖", - "setting_image_viewer_preview_subtitle": "啟用以載入中等品質的圖片,停用以載入原圖或縮圖。", + "setting_image_viewer_preview_subtitle": "啟用以載入中等解析度圖片。停用則直接載入原圖或僅使用縮圖。", "setting_image_viewer_preview_title": "載入預覽圖", "setting_image_viewer_title": "圖片", "setting_languages_apply": "套用", @@ -2022,10 +2043,10 @@ "setting_notifications_subtitle": "調整通知選項", "setting_notifications_total_progress_subtitle": "總體上傳進度 (已完成/總計)", "setting_notifications_total_progress_title": "顯示背景備份總進度", - "setting_video_viewer_auto_play_subtitle": "打開視頻時自動開始播放", - "setting_video_viewer_auto_play_title": "自動播放視頻", - "setting_video_viewer_looping_title": "迴圈播放", - "setting_video_viewer_original_video_subtitle": "從伺服器串流影片時,優先播放原始畫質(即使有轉檔的版本可用)。這可能會導致播放時出現緩衝情況。若影片已儲存在本機,則一律以原始畫質播放,與此設定無關。", + "setting_video_viewer_auto_play_subtitle": "開啟影片時自動開始播放", + "setting_video_viewer_auto_play_title": "自動播放影片", + "setting_video_viewer_looping_title": "循環播放", + "setting_video_viewer_original_video_subtitle": "從伺服器串流影片時,即使已有轉碼版本,仍優先播放原始畫質。這可能會導致緩衝。儲存於本機的影片則一律以原始畫質播放,不受此設定影響。", "setting_video_viewer_original_video_title": "一律播放原始影片", "settings": "設定", "settings_require_restart": "請重啟 Immich 以使設定生效", @@ -2034,11 +2055,11 @@ "share": "分享", "share_action_prompt": "已分享了 {count} 個項目", "share_add_photos": "新增項目", - "share_assets_selected": "{count} 已選擇", + "share_assets_selected": "已選取 {count} 項", "share_dialog_preparing": "正在準備...", "share_link": "分享連結", "shared": "共享", - "shared_album_activities_input_disable": "已停用評論", + "shared_album_activities_input_disable": "留言功能已停用", "shared_album_activity_remove_content": "您確定要刪除此活動嗎?", "shared_album_activity_remove_title": "刪除活動", "shared_album_section_people_action_error": "結束/刪除相簿失敗", @@ -2046,14 +2067,14 @@ "shared_album_section_people_action_remove_user": "從相簿中刪除使用者", "shared_album_section_people_title": "人物", "shared_by": "共享自", - "shared_by_user": "由 {user} 分享", - "shared_by_you": "由您分享", - "shared_from_partner": "來自 {partner} 的照片", + "shared_by_user": "由 {user} 共享", + "shared_by_you": "由您共享", + "shared_from_partner": "來自 {partner} 的相片", "shared_intent_upload_button_progress_text": "{current} / {total} 已上傳", - "shared_link_app_bar_title": "共享連結", - "shared_link_clipboard_copied_massage": "複製到剪貼簿", + "shared_link_app_bar_title": "分享連結", + "shared_link_clipboard_copied_massage": "已複製到剪貼簿", "shared_link_clipboard_text": "連結: {link}\n密碼: {password}", - "shared_link_create_error": "新增共享連結時發生錯誤", + "shared_link_create_error": "建立分享連結時發生錯誤", "shared_link_custom_url_description": "使用自訂 URL", "shared_link_edit_description_hint": "編輯共享描述", "shared_link_edit_expire_after_option_day": "1 天", @@ -2078,22 +2099,22 @@ "shared_link_expires_seconds": "將在 {count} 秒後過期", "shared_link_individual_shared": "個人共享", "shared_link_info_chip_metadata": "EXIF", - "shared_link_manage_links": "管理共享連結", - "shared_link_options": "共享連結選項", - "shared_link_password_description": "要求在存取此連結時提供密碼", - "shared_links": "共享連結", - "shared_links_description": "以連結分享照片和影片", - "shared_photos_and_videos_count": "{assetCount, plural, other {已分享 # 張照片及影片。}}", + "shared_link_manage_links": "管理分享連結", + "shared_link_options": "分享連結選項", + "shared_link_password_description": "存取此分享連結時要求密碼", + "shared_links": "分享連結", + "shared_links_description": "以連結分享相片和影片", + "shared_photos_and_videos_count": "{assetCount, plural, other {已分享 # 張相片及影片。}}", "shared_with_me": "與我共享", "shared_with_partner": "與 {partner} 共享", "sharing": "共享", "sharing_enter_password": "要檢視此頁面請輸入密碼。", "sharing_page_album": "共享相簿", - "sharing_page_description": "新增共享相簿以與網路中的人共享照片和短片。", + "sharing_page_description": "建立共享相簿,與您網路中的成員分享相片與影片。", "sharing_page_empty_list": "空白清單", "sharing_sidebar_description": "在側邊欄顯示共享連結", - "sharing_silver_appbar_create_shared_album": "新增共享相簿", - "sharing_silver_appbar_share_partner": "共享給親朋好友", + "sharing_silver_appbar_create_shared_album": "建立共享相簿", + "sharing_silver_appbar_share_partner": "與親友共享", "shift_to_permanent_delete": "按 ⇧ 永久刪除檔案", "show_album_options": "顯示相簿選項", "show_albums": "顯示相簿", @@ -2103,7 +2124,7 @@ "show_gallery": "顯示畫廊", "show_hidden_people": "顯示隱藏的人物", "show_in_timeline": "在時間軸中顯示", - "show_in_timeline_setting_description": "在您的時間軸中顯示這位使用者的照片和影片", + "show_in_timeline_setting_description": "在您的時間軸中顯示這位使用者的相片和影片", "show_keyboard_shortcuts": "顯示鍵盤快捷鍵", "show_metadata": "顯示中繼資料", "show_or_hide_info": "顯示或隱藏資訊", @@ -2112,11 +2133,11 @@ "show_progress_bar": "顯示進度條", "show_schema": "顯示架構", "show_search_options": "顯示搜尋選項", - "show_shared_links": "顯示共享連結", + "show_shared_links": "顯示分享連結", "show_slideshow_transition": "顯示幻燈片轉場", - "show_supporter_badge": "擁護者徽章", - "show_supporter_badge_description": "顯示擁護者徽章", - "show_text_recognition": "顯示文字識別", + "show_supporter_badge": "支持者徽章", + "show_supporter_badge_description": "顯示支持者徽章", + "show_text_recognition": "顯示文字辨識", "show_text_search_menu": "顯示文字蒐索選單", "shuffle": "隨機排序", "sidebar": "側邊欄", @@ -2128,22 +2149,24 @@ "skip_to_folders": "跳到資料夾", "skip_to_tags": "跳轉到標籤", "slideshow": "幻燈片", + "slideshow_repeat": "重複投影片", + "slideshow_repeat_description": "循環播放", "slideshow_settings": "幻燈片設定", "sort_albums_by": "相簿排序依據...", "sort_created": "建立日期", "sort_items": "項目數量", "sort_modified": "日期已修改", "sort_newest": "最新的相片", - "sort_oldest": "最舊的照片", - "sort_people_by_similarity": "按相似度排序人員", - "sort_recent": "最新的照片", + "sort_oldest": "最舊的相片", + "sort_people_by_similarity": "依相似度排序人物", + "sort_recent": "最新的相片", "sort_title": "標題", "source": "來源", "stack": "堆疊", "stack_action_prompt": "已堆疊了{count} 個項目", "stack_duplicates": "堆疊重複項目", - "stack_select_one_photo": "為堆疊選一張主要照片", - "stack_selected_photos": "堆疊所選的照片", + "stack_select_one_photo": "為堆疊選一張主要相片", + "stack_selected_photos": "堆疊選取的相片", "stacked_assets_count": "已堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", "stacktrace": "堆疊追蹤", "start": "開始", @@ -2152,10 +2175,10 @@ "state": "地區", "status": "狀態", "stop_casting": "停止投放", - "stop_motion_photo": "停止動態照片", - "stop_photo_sharing": "要停止分享您的照片嗎?", - "stop_photo_sharing_description": "{partner} 將無法再存取您的照片。", - "stop_sharing_photos_with_user": "停止與此使用者共享您的照片", + "stop_motion_photo": "停止動態相片", + "stop_photo_sharing": "要停止分享您的相片嗎?", + "stop_photo_sharing_description": "{partner} 將無法再存取您的相片。", + "stop_sharing_photos_with_user": "停止與此使用者共享您的相片", "storage": "儲存空間", "storage_label": "儲存標籤", "storage_quota": "儲存空間", @@ -2167,19 +2190,20 @@ "support": "支援", "support_and_feedback": "支援與回饋", "support_third_party_description": "您安裝的 Immich 是由第三方打包的。您遇到的問題可能是該套件造成的,所以請先使用下面的連結向他們提出問題。", + "supporter": "支持者", "swap_merge_direction": "交換合併方向", "sync": "同步", "sync_albums": "同步相簿", - "sync_albums_manual_subtitle": "將所有上傳的短片和照片同步到選定的備份相簿", + "sync_albums_manual_subtitle": "將所有上傳的影片與相片同步至選取的備份相簿", "sync_local": "同步本機", "sync_remote": "同步遠端", "sync_status": "同步狀態", "sync_status_subtitle": "檢視和管理同步系統", - "sync_upload_album_setting_subtitle": "新增照片和短片並上傳到 Immich 上的選定相簿中", + "sync_upload_album_setting_subtitle": "建立並上傳相片與影片至 Immich 上選取的相簿", "tag": "標籤", "tag_assets": "標記檔案", "tag_created": "已建立標籤:{tag}", - "tag_feature_description": "以邏輯標記要旨分類瀏覽照片和影片", + "tag_feature_description": "以邏輯標記要旨分類瀏覽相片和影片", "tag_not_found_question": "找不到標籤?建立新標籤。", "tag_people": "標籤人物", "tag_updated": "已更新標籤:{tag}", @@ -2187,7 +2211,7 @@ "tags": "標籤", "tap_to_run_job": "點選以進行作業", "template": "模板", - "text_recognition": "文字識別", + "text_recognition": "文字辨識", "theme": "主題", "theme_selection": "主題選項", "theme_selection_description": "依瀏覽器系統偏好自動設定深、淺色主題", @@ -2204,6 +2228,7 @@ "theme_setting_theme_subtitle": "選擇套用主題", "theme_setting_three_stage_loading_subtitle": "三段式載入可能提升載入效能,但會大幅增加網路負載", "theme_setting_three_stage_loading_title": "啟用三段式載入", + "then": "然後", "they_will_be_merged_together": "它們將會被合併在一起", "third_party_resources": "第三方資源", "time": "時間", @@ -2229,21 +2254,21 @@ "trash_count": "丟掉 {count, number} 個檔案", "trash_delete_asset": "將檔案丟進垃圾桶 / 刪除", "trash_emptied": "已清空回收桶", - "trash_no_results_message": "垃圾桶中的照片和影片將顯示在這裡。", + "trash_no_results_message": "垃圾桶中的相片和影片將顯示在這裡。", "trash_page_delete_all": "刪除全部", "trash_page_empty_trash_dialog_content": "是否清空回收桶?這些項目將被從 Immich 中永久刪除", "trash_page_info": "回收桶中項目將在 {days} 天後永久刪除", "trash_page_no_assets": "暫無已刪除項目", - "trash_page_restore_all": "恢復全部", + "trash_page_restore_all": "全部還原", "trash_page_select_assets_btn": "選擇項目", "trash_page_title": "垃圾桶 ({count})", "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", "trigger": "觸發", - "trigger_asset_uploaded": "資產已上傳", - "trigger_asset_uploaded_description": "上傳新資產時觸發", - "trigger_description": "啟動工作流的事件", - "trigger_person_recognized": "被認可的人", - "trigger_person_recognized_description": "當檢測到有人時觸發", + "trigger_asset_uploaded": "項目已上傳", + "trigger_asset_uploaded_description": "在上傳新項目時觸發", + "trigger_description": "觸發工作流程的事件", + "trigger_person_recognized": "已辨識人物", + "trigger_person_recognized_description": "偵測到人物時觸發", "trigger_type": "觸發類型", "troubleshoot": "疑難解答", "type": "類型", @@ -2276,19 +2301,20 @@ "unstack": "取消堆疊", "unstack_action_prompt": "{count} 個取消堆疊", "unstacked_assets_count": "已解除堆疊 {count, plural, other {# 個檔案}}", - "unsupported_field_type": "不支持的欄位類型", + "unsupported_field_type": "不支援的欄位類型", "untagged": "無標籤", - "untitled_workflow": "無標題工作流", + "untitled_workflow": "未命名工作流程", "up_next": "下一個", - "update_location_action_prompt": "使用以下命令更新{count}個所選資產的位置:", + "update_location_action_prompt": "更新 {count} 個所選項目的位置:", "updated_at": "更新於", "updated_password": "已更新密碼", "upload": "上傳", - "upload_concurrency": "上傳並行", + "upload_concurrency": "上傳並行數", "upload_details": "上傳詳細資訊", "upload_dialog_info": "是否要將所選項目備份到伺服器?", "upload_dialog_title": "上傳項目", - "upload_errors": "上傳完成,但有 {count, plural, other {# 處時發生錯誤}},要檢視新上傳的檔案請重新整理頁面。", + "upload_error_with_count": "{count, plural, one {# 個項目} other {# 個項目}}上傳錯誤", + "upload_errors": "上傳完成,但有 {count, plural, other {# 個錯誤}}。請重新整理頁面以查看新上傳的項目。", "upload_finished": "上傳完成", "upload_progress": "剩餘 {remaining, number} - 已處理 {processed, number}/{total, number}", "upload_skipped_duplicates": "已略過 {count, plural, other {# 個重複的檔案}}", @@ -2298,7 +2324,7 @@ "upload_success": "上傳成功,要檢視新上傳的檔案請重新整理頁面。", "upload_to_immich": "上傳至 Immich ({count})", "uploading": "上傳中", - "uploading_media": "媒體上傳中", + "uploading_media": "項目上傳中", "url": "網址", "usage": "用量", "use_biometric": "使用生物辨識", @@ -2307,11 +2333,11 @@ "user": "使用者", "user_has_been_deleted": "此使用者已被刪除。", "user_id": "使用者 ID", - "user_liked": "{user} 喜歡了 {type, select, photo {這張照片} video {這段影片} asset {這個檔案} other {它}}", + "user_liked": "{user} 喜歡了 {type, select, photo {這張相片} video {這段影片} asset {這個檔案} other {它}}", "user_pin_code_settings": "PIN 碼", "user_pin_code_settings_description": "管理您的 PIN 碼", "user_privacy": "使用者隱私", - "user_purchase_settings": "購置", + "user_purchase_settings": "購買", "user_purchase_settings_description": "管理您的購買", "user_role_set": "設 {user} 為{role}", "user_usage_detail": "使用者用量詳細資訊", @@ -2327,12 +2353,12 @@ "variables": "變數", "version": "版本", "version_announcement_closing": "敬祝順心,Alex", - "version_announcement_message": "嗨~新版本的 Immich 推出了。為防止設定發生錯誤,請花點時間閱讀發行說明,並確保設定是最新的,特別是使用 WatchTower 等自動更新工具時。", + "version_announcement_message": "嗨!新版本的 Immich 已發布。請花點時間閱讀 發行說明 並確保您的設定是最新的,以防止任何設定錯誤,特別是如果您使用 WatchTower 或任何自動處理 Immich 執行個體更新的機制。", "version_history": "版本紀錄", "version_history_item": "{date} 安裝了 {version}", "video": "影片", "video_hover_setting": "遊標停留時播放影片縮圖", - "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用,將滑鼠停在播放圖示上也可以播放。", + "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用此功能,仍可透過將滑鼠停在播放圖示上來開始播放。", "videos": "影片", "videos_count": "{count, plural, other {# 部影片}}", "videos_only": "只允許影片", @@ -2340,7 +2366,7 @@ "view_album": "檢視相簿", "view_all": "瀏覽全部", "view_all_users": "檢視所有使用者", - "view_asset_owners": "查看資產所有者", + "view_asset_owners": "查看項目擁有者", "view_details": "檢視詳細資訊", "view_in_timeline": "在時間軸中檢視", "view_link": "檢視連結", @@ -2349,7 +2375,7 @@ "view_next_asset": "檢視下一項", "view_previous_asset": "檢視上一項", "view_qr_code": "檢視 QR code", - "view_similar_photos": "檢視相似照片", + "view_similar_photos": "檢視相似相片", "view_stack": "檢視堆疊", "view_user": "顯示使用者", "viewer_remove_from_stack": "從堆疊中移除", @@ -2366,26 +2392,26 @@ "welcome_to_immich": "歡迎使用 Immich", "width": "寬度", "wifi_name": "Wi-Fi 名稱", - "workflow_delete_prompt": "您確定要删除此工作流嗎?", - "workflow_deleted": "工作流已删除", - "workflow_description": "工作流描述", - "workflow_info": "工作流資訊", - "workflow_json": "工作流程JSON", - "workflow_json_help": "以JSON格式編輯工作流配寘。 更改將同步到視覺化構建器。", - "workflow_name": "工作流名稱", - "workflow_navigation_prompt": "您確定不保存更改就離開嗎?", - "workflow_summary": "工作流摘要", - "workflow_update_success": "工作流已成功更新", - "workflow_updated": "工作流已更新", - "workflows": "工作流", - "workflows_help_text": "工作流根據觸發器和篩檢程式自動執行資產操作", + "workflow_delete_prompt": "確定要刪除此工作流程嗎?", + "workflow_deleted": "工作流程已刪除", + "workflow_description": "工作流程說明", + "workflow_info": "工作流程詳細資訊", + "workflow_json": "工作流程 JSON", + "workflow_json_help": "以 JSON 格式編輯工作流程設定。變更將同步至視覺化編輯器。", + "workflow_name": "工作流程名稱", + "workflow_navigation_prompt": "您確定要不儲存變更就離開嗎?", + "workflow_summary": "工作流程摘要", + "workflow_update_success": "已成功更新工作流程", + "workflow_updated": "工作流程已更新", + "workflows": "工作流程", + "workflows_help_text": "根據觸發條件與篩選器自動執行動作", "wrong_pin_code": "PIN 碼錯誤", "year": "年", "years_ago": "{years, plural, other {# 年}}前", "yes": "是", - "you_dont_have_any_shared_links": "您沒有任何共享連結", + "you_dont_have_any_shared_links": "您沒有任何分享連結", "your_wifi_name": "您的 Wi-Fi 名稱", - "zero_to_clear_rating": "按0清除資產評星", + "zero_to_clear_rating": "按 0 以清除項目評分", "zoom_image": "縮放圖片", "zoom_to_bounds": "縮放到邊界" } diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 9db6fd78dd..efbe710dbc 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,8 +1,8 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:667cf70698924920f29ebdb8d749ab665811503b87093d4f11826d114fd7255e AS builder-cpu +FROM python:3.11-bookworm@sha256:aa23850b91cb4c7faedac8ca9aa74ddc6eb03529a519145a589a7f35df4c5927 AS builder-cpu -FROM python:3.13-slim-trixie@sha256:0222b795db95bf7412cede36ab46a266cfb31f632e64051aac9806dabf840a61 AS builder-openvino +FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS builder-openvino FROM builder-cpu AS builder-cuda @@ -83,12 +83,12 @@ RUN if [ "$DEVICE" = "rocm" ]; then \ uv pip install /opt/onnxruntime_rocm-*.whl; \ fi -FROM python:3.11-slim-bookworm@sha256:917ec0e42cd6af87657a768449c2f604a6b67c7ab8e10ff917b8724799f816d3 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \ MACHINE_LEARNING_MODEL_ARENA=false -FROM python:3.13-slim-trixie@sha256:0222b795db95bf7412cede36ab46a266cfb31f632e64051aac9806dabf840a61 AS prod-openvino +FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index ed34c6a338..e3d24ce172 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "immich-ml" -version = "2.5.5" +version = "2.5.6" description = "" authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }] requires-python = ">=3.11,<4.0" @@ -15,7 +15,7 @@ dependencies = [ "numpy>=2.3.4", "opencv-python-headless>=4.7.0.72,<5.0", "orjson>=3.9.5", - "pillow>=9.5.0,<11.0", + "pillow>=12.1.1,<12.2", "pydantic>=2.0.0,<3", "pydantic-settings>=2.5.2,<3", "python-multipart>=0.0.6,<1.0", diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index d0b502283f..25f59a8fe5 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -379,50 +379,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.4" +version = "7.13.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716, upload-time = "2024-10-20T22:57:39.682Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819, upload-time = "2024-10-20T22:56:20.132Z" }, - { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263, upload-time = "2024-10-20T22:56:21.88Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205, upload-time = "2024-10-20T22:56:23.03Z" }, - { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612, upload-time = "2024-10-20T22:56:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479, upload-time = "2024-10-20T22:56:26.749Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405, upload-time = "2024-10-20T22:56:27.958Z" }, - { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038, upload-time = "2024-10-20T22:56:29.816Z" }, - { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812, upload-time = "2024-10-20T22:56:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400, upload-time = "2024-10-20T22:56:33.569Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243, upload-time = "2024-10-20T22:56:34.863Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013, upload-time = "2024-10-20T22:56:36.034Z" }, - { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251, upload-time = "2024-10-20T22:56:38.054Z" }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268, upload-time = "2024-10-20T22:56:40.051Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298, upload-time = "2024-10-20T22:56:41.929Z" }, - { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367, upload-time = "2024-10-20T22:56:43.141Z" }, - { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853, upload-time = "2024-10-20T22:56:44.33Z" }, - { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160, upload-time = "2024-10-20T22:56:46.258Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824, upload-time = "2024-10-20T22:56:48.666Z" }, - { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639, upload-time = "2024-10-20T22:56:50.664Z" }, - { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428, upload-time = "2024-10-20T22:56:52.468Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039, upload-time = "2024-10-20T22:56:53.656Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298, upload-time = "2024-10-20T22:56:54.979Z" }, - { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813, upload-time = "2024-10-20T22:56:56.209Z" }, - { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959, upload-time = "2024-10-20T22:56:58.06Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950, upload-time = "2024-10-20T22:56:59.329Z" }, - { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610, upload-time = "2024-10-20T22:57:00.645Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697, upload-time = "2024-10-20T22:57:01.944Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541, upload-time = "2024-10-20T22:57:03.848Z" }, - { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707, upload-time = "2024-10-20T22:57:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439, upload-time = "2024-10-20T22:57:06.35Z" }, - { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784, upload-time = "2024-10-20T22:57:07.857Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058, upload-time = "2024-10-20T22:57:09.845Z" }, - { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772, upload-time = "2024-10-20T22:57:11.147Z" }, - { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490, upload-time = "2024-10-20T22:57:13.02Z" }, - { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848, upload-time = "2024-10-20T22:57:14.927Z" }, - { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340, upload-time = "2024-10-20T22:57:16.246Z" }, - { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229, upload-time = "2024-10-20T22:57:17.546Z" }, - { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510, upload-time = "2024-10-20T22:57:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353, upload-time = "2024-10-20T22:57:20.891Z" }, - { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502, upload-time = "2024-10-20T22:57:22.21Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] @@ -472,17 +523,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd [[package]] name = "fastapi" -version = "0.127.1" +version = "0.128.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/8a/6b9ba6eb8ff3817caae83120495965d9e70afb4d6348cb120e464ee199f4/fastapi-0.127.1.tar.gz", hash = "sha256:946a87ee5d931883b562b6bada787d6c8178becee2683cb3f9b980d593206359", size = 391876, upload-time = "2025-12-26T13:04:47.075Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/f3/a6858d147ed2645c095d11dc2440f94a5f1cd8f4df888e3377e6b5281a0f/fastapi-0.127.1-py3-none-any.whl", hash = "sha256:31d670a4f9373cc6d7994420f98e4dc46ea693145207abc39696746c83a44430", size = 112332, upload-time = "2025-12-26T13:04:45.329Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, ] [[package]] @@ -829,7 +881,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.34.4" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -841,9 +893,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/c9/bdbe19339f76d12985bc03572f330a01a93c04dffecaaea3061bdd7fb892/huggingface_hub-0.34.4.tar.gz", hash = "sha256:a4228daa6fb001be3f4f4bdaf9a0db00e1739235702848df00885c9b5742c85c", size = 459768, upload-time = "2025-08-08T09:14:52.365Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/7b/bb06b061991107cd8783f300adff3e7b7f284e330fd82f507f2a1417b11d/huggingface_hub-0.34.4-py3-none-any.whl", hash = "sha256:9b365d781739c93ff90c359844221beef048403f1bc1f1c123c191257c3c890a", size = 561452, upload-time = "2025-08-08T09:14:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, ] [[package]] @@ -882,7 +934,7 @@ wheels = [ [[package]] name = "immich-ml" -version = "2.5.5" +version = "2.5.6" source = { editable = "." } dependencies = [ { name = "aiocache" }, @@ -978,7 +1030,7 @@ requires-dist = [ { name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.23.0,<2" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "orjson", specifier = ">=3.9.5" }, - { name = "pillow", specifier = ">=9.5.0,<11.0" }, + { name = "pillow", specifier = ">=12.1.1,<12.2" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "pydantic-settings", specifier = ">=2.5.2,<3" }, { name = "python-multipart", specifier = ">=0.0.6,<1.0" }, @@ -1205,7 +1257,7 @@ wheels = [ [[package]] name = "locust" -version = "2.42.6" +version = "2.43.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1214,7 +1266,6 @@ dependencies = [ { name = "flask-login" }, { name = "gevent" }, { name = "geventhttpclient" }, - { name = "locust-cloud" }, { name = "msgpack" }, { name = "psutil" }, { name = "pytest" }, @@ -1226,25 +1277,9 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/19/dd816835679c80eba9c339a4bfcb6380fa8b059a5da45894ac80d73bc504/locust-2.42.6.tar.gz", hash = "sha256:fa603f4ac1c48b9ac56f4c34355944ebfd92590f4197b6d126ea216bd81cc036", size = 1418806, upload-time = "2025-11-29T17:40:10.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c5/7d7bd50ac744bc209a4bcbeb74660d7ae450a44441737efe92ee9d8ea6a7/locust-2.43.3.tar.gz", hash = "sha256:b5d2c48f8f7d443e3abdfdd6ec2f7aebff5cd74fab986bcf1e95b375b5c5a54b", size = 1445349, upload-time = "2026-02-12T09:55:34.591Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/4f/be2b7b87a4cea00d89adabeee5c61e8831c2af8a0eca3cbe931516f0e155/locust-2.42.6-py3-none-any.whl", hash = "sha256:2d02502489c8a2e959e2ca4b369c81bbd6b9b9e831d9422ab454541a3c2c6252", size = 1437376, upload-time = "2025-11-29T17:40:08.37Z" }, -] - -[[package]] -name = "locust-cloud" -version = "1.30.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "configargparse" }, - { name = "gevent" }, - { name = "platformdirs" }, - { name = "python-engineio" }, - { name = "python-socketio", extra = ["client"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/86/cd6b611f008387ffce5bcb6132ba7431aec7d1b09d8ce27e152e96d94315/locust_cloud-1.30.0.tar.gz", hash = "sha256:324ae23754d49816df96d3f7472357a61cd10e56cebcb26e2def836675cb3c68", size = 457297, upload-time = "2025-12-15T13:35:50.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/db/35c1cc8e01dfa570913255c55eb983a7e2e532060b4d1ee5f1fb543a6a0b/locust_cloud-1.30.0-py3-none-any.whl", hash = "sha256:2324b690efa1bfc8d1871340276953cf265328bd6333e07a5ba8ff7dc5e99e6c", size = 413446, upload-time = "2025-12-15T13:35:48.75Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d2/dc5379876d3a481720803653ea4d219f0c26f2d2b37c9243baaa16d0bc79/locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553", size = 1463473, upload-time = "2026-02-12T09:55:31.727Z" }, ] [[package]] @@ -1495,83 +1530,81 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.4" +version = "2.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, - { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, - { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, - { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, - { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, - { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, - { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, - { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, - { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, - { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, - { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, - { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, - { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, - { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, - { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, - { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, - { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, - { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, - { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, - { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, - { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, - { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, - { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, - { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, - { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, - { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, - { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, - { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, - { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, - { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, - { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] [[package]] @@ -1626,10 +1659,9 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.23.2" +version = "1.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coloredlogs" }, { name = "flatbuffers" }, { name = "numpy" }, { name = "packaging" }, @@ -1637,31 +1669,33 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" }, - { url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" }, - { url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" }, - { url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" }, - { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" }, - { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" }, - { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" }, + { url = "https://files.pythonhosted.org/packages/d2/88/d9757c62a0f96b5193f8d447a141eefd14498c404cc5caf1a6f3233cf102/onnxruntime-1.24.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:79b3119ab9f4f3817062e6dbe7f4a44937de93905e3a31ba34313d18cb49e7be", size = 17212018, upload-time = "2026-02-05T17:32:13.986Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/b3305c39144e19dbe8791802076b29b4b592b09de03d0e340c1314bfd408/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86bc43e922b1f581b3de26a3dc402149c70e5542fceb5bec6b3a85542dbeb164", size = 15018703, upload-time = "2026-02-05T17:30:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/d273b75fe7825ea3feed321dd540aef33d8a1380ddd8ac3bb70a8ed000fe/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cabe71ca14dcfbf812d312aab0a704507ac909c137ee6e89e4908755d0fc60e", size = 17096352, upload-time = "2026-02-05T17:31:29.057Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/0616101a3938bfe2918ea60b581a9bbba61ffc255c63388abb0885f7ce18/onnxruntime-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:3273c330f5802b64b4103e87b5bbc334c0355fff1b8935d8910b0004ce2f20c8", size = 12493235, upload-time = "2026-02-05T17:32:04.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/30/437de870e4e1c6d237a2ca5e11f54153531270cb5c745c475d6e3d5c5dcf/onnxruntime-1.24.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7307aab9e2e879c0171f37e0eb2808a5b4aec7ba899bb17c5f0cedfc301a8ac2", size = 17211043, upload-time = "2026-02-05T17:32:16.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/60/004401cd86525101ad8aa9eec301327426555d7a77fac89fd991c3c7aae6/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780add442ce2d4175fafb6f3102cdc94243acffa3ab16eacc03dd627cc7b1b54", size = 15016224, upload-time = "2026-02-05T17:30:56.791Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a1/43ad01b806a1821d1d6f98725edffcdbad54856775643718e9124a09bfbe/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6119526eda12613f0d0498e2ae59563c247c370c9cef74c2fc93133dde157", size = 17098191, upload-time = "2026-02-05T17:31:31.87Z" }, + { url = "https://files.pythonhosted.org/packages/ff/37/5beb65270864037d5c8fb25cfe6b23c48b618d1f4d06022d425cbf29bd9c/onnxruntime-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0af2f1cfcfff9094971c7eb1d1dfae7ccf81af197493c4dc4643e4342c0946", size = 12493108, upload-time = "2026-02-05T17:32:07.076Z" }, + { url = "https://files.pythonhosted.org/packages/95/77/7172ecfcbdabd92f338e694f38c325f6fab29a38fa0a8c3d1c85b9f4617c/onnxruntime-1.24.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:82e367770e8fba8a87ba9f4c04bb527e6d4d7204540f1390f202c27a3b759fb4", size = 17211381, upload-time = "2026-02-05T17:31:09.601Z" }, + { url = "https://files.pythonhosted.org/packages/79/5b/532a0d75b93bbd0da0e108b986097ebe164b84fbecfdf2ddbf7c8a3a2e83/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1099f3629832580fedf415cfce2462a56cc9ca2b560d6300c24558e2ac049134", size = 15016000, upload-time = "2026-02-05T17:31:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b5/40606c7bce0702975a077bc6668cd072cd77695fc5c0b3fcf59bdb1fe65e/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6361dda4270f3939a625670bd67ae0982a49b7f923207450e28433abc9c3a83b", size = 17097637, upload-time = "2026-02-05T17:31:34.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/9e8f7933796b466241b934585723c700d8fb6bde2de856e65335193d7c93/onnxruntime-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:bd1e4aefe73b6b99aa303cd72562ab6de3cccb09088100f8ad1c974be13079c7", size = 12492467, upload-time = "2026-02-05T17:32:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8a/ee07d86e35035f9fed42497af76435f5a613d4e8b6c537ea0f8ef9fa85da/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88a2b54dca00c90fca6303eedf13d49b5b4191d031372c2e85f5cffe4d86b79e", size = 15025407, upload-time = "2026-02-05T17:31:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/ab3e1dda4b126313d240e1aaa87792ddb1f5ba6d03ca2f093a7c4af8c323/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dfbba602da840615ed5b431facda4b3a43b5d8276cf9e0dbf13d842df105838", size = 17099810, upload-time = "2026-02-05T17:31:37.537Z" }, + { url = "https://files.pythonhosted.org/packages/87/23/167d964414cee2af9c72af323b28d2c4cb35beed855c830a23f198265c79/onnxruntime-1.24.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:890c503ca187bc883c3aa72c53f2a604ec8e8444bdd1bf6ac243ec6d5e085202", size = 17214004, upload-time = "2026-02-05T17:31:11.917Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/6e5558fdd51027d6830cf411bc003ae12c64054826382e2fab89e99486a0/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da1b84b3bdeec543120df169e5e62a1445bf732fc2c7fb036c2f8a4090455e8", size = 15017034, upload-time = "2026-02-05T17:31:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/3cb1c9eaae1103265ed7eb00a3eaeb0d9ba51dc88edc398b7071c9553bed/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:557753ec345efa227c6a65139f3d29c76330fcbd54cc10dd1b64232ebb939c13", size = 17097531, upload-time = "2026-02-05T17:31:40.303Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/4522b199c12db7c5b46aaf265ee0d741abe65ea912f6c0aaa2cc18a4654d/onnxruntime-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:ea4942104805e868f3ddddfa1fbb58b04503a534d489ab2d1452bbfa345c78c2", size = 12795556, upload-time = "2026-02-05T17:32:11.886Z" }, + { url = "https://files.pythonhosted.org/packages/a1/53/3b8969417276b061ff04502ccdca9db4652d397abbeb06c9f6ae05cec9ca/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea8963a99e0f10489acdf00ef3383c3232b7e44aa497b063c63be140530d9f85", size = 15025434, upload-time = "2026-02-05T17:31:06.942Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/cfcf009eb38d90cc628c087b6506b3dfe1263387f3cbbf8d272af4fef957/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34488aa760fb5c2e6d06a7ca9241124eb914a6a06f70936a14c669d1b3df9598", size = 17099815, upload-time = "2026-02-05T17:31:43.092Z" }, ] [[package]] name = "onnxruntime-gpu" -version = "1.23.2" +version = "1.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coloredlogs" }, { name = "flatbuffers" }, { name = "numpy" }, { name = "packaging" }, @@ -1669,13 +1703,16 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/43/a4/e3d7fbe32b44e814ae24ed642f05fac5d96d120efd82db7a7cac936e85a9/onnxruntime_gpu-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d76d1ac7a479ecc3ac54482eea4ba3b10d68e888a0f8b5f420f0bdf82c5eec59", size = 300525715, upload-time = "2025-10-22T16:56:19.928Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5c/dba7c009e73dcce02e7f714574345b5e607c5c75510eb8d7bef682b45e5d/onnxruntime_gpu-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:054282614c2fc9a4a27d74242afbae706a410f1f63cc35bc72f99709029a5ba4", size = 244506823, upload-time = "2025-10-22T16:55:09.526Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d9/b7140a4f1615195938c7e358c0804bb84271f0d6886b5cbf105c6cb58aae/onnxruntime_gpu-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f2d1f720685d729b5258ec1b36dee1de381b8898189908c98cbeecdb2f2b5c2", size = 300509596, upload-time = "2025-10-22T16:56:31.728Z" }, - { url = "https://files.pythonhosted.org/packages/87/da/2685c79e5ea587beddebe083601fead0bdf3620bc2f92d18756e7de8a636/onnxruntime_gpu-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:fe925a84b00e291e0ad3fac29bfd8f8e06112abc760cdc82cb711b4f3935bd95", size = 244508327, upload-time = "2025-10-22T16:55:19.397Z" }, - { url = "https://files.pythonhosted.org/packages/03/05/40d561636e4114b54aa06d2371bfbca2d03e12cfdf5d4b85814802f18a75/onnxruntime_gpu-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e8f75af5da07329d0c3a5006087f4051d8abd133b4be7c9bae8cdab7bea4c26", size = 300515567, upload-time = "2025-10-22T16:56:43.794Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3b/418300438063d403384c79eaef1cb13c97627042f2247b35a887276a355a/onnxruntime_gpu-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:7f1b3f49e5e126b99e23ec86b4203db41c2a911f6165f7624f2bc8267aaca767", size = 244507535, upload-time = "2025-10-22T16:55:28.532Z" }, - { url = "https://files.pythonhosted.org/packages/b8/dc/80b145e3134d7eba31309b3299a2836e37c76e4c419a261ad9796f8f8d65/onnxruntime_gpu-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20959cd4ae358aab6579ab9123284a7b1498f7d51ec291d429a5edc26511306f", size = 300525759, upload-time = "2025-10-22T16:56:56.925Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c7/07d06175f1124fc89e8b7da30d70eb8e0e1400d90961ae1cbea9da69e69b/onnxruntime_gpu-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac4bfc90c376516b13d709764ab257e4e3d78639bf6a2ccfc826e9db4a5c7ddf", size = 252616647, upload-time = "2026-02-05T17:24:02.993Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/47c2a873bf5fc307cda696e8a8cb54b7c709f5a4b3f9e2b4a636066a63c2/onnxruntime_gpu-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:ccd800875cb6c04ce623154c7fa312da21631ef89a9543c9a21593817cfa3473", size = 207089749, upload-time = "2026-02-05T17:23:59.5Z" }, + { url = "https://files.pythonhosted.org/packages/db/a8/fb1a36a052321a839cc9973f6cfd630709412a24afff2d7315feb3efc4b8/onnxruntime_gpu-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:710bf83751e6761584ad071102af3cbffd4b42bb77b2e3caacfb54ffbaa0666b", size = 252628733, upload-time = "2026-02-05T17:24:12.926Z" }, + { url = "https://files.pythonhosted.org/packages/52/65/48f694b81a963f3ee575041d5f2879b15268f5e7e14d90c3e671836c9646/onnxruntime_gpu-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:b128a42b3fa098647765ba60c2af9d4bf839181307cfac27da649364feb37f7b", size = 207089008, upload-time = "2026-02-05T17:24:07.126Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e7/4e19062e95d3701c0d32c228aa848ba4a1cc97651e53628d978dba8e1267/onnxruntime_gpu-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db9acb0d0e59d93b4fa6b7fd44284ece4408d0acee73235d43ed343f8cee7ee5", size = 252629216, upload-time = "2026-02-05T17:24:24.604Z" }, + { url = "https://files.pythonhosted.org/packages/c4/82/223d7120d8a98b07c104ddecfb0cc2536188e566a4e9c2dee7572453f89c/onnxruntime_gpu-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:59fdb40743f0722f3b859209f649ea160ca6bb42799e43f49b70a3ec5fc8c4ad", size = 207089285, upload-time = "2026-02-05T17:24:18.497Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/3159e57f09d7e6c8ad47d8ba8d5bd7494f383bc1071481cf38c9c8142bf9/onnxruntime_gpu-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88ca04e1dffea2d4c3c79cf4de7f429e99059d085f21b3e775a8d36380cd5186", size = 252633977, upload-time = "2026-02-05T17:24:33.568Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/51ad0ab878ff1456a831a0566b4db982a904e22f138e4b2c5f021bac517f/onnxruntime_gpu-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ced66900b1f48bddb62b5233925c3b56f8e008e2c34ebf8c060b20cae5842bcf", size = 252629039, upload-time = "2026-02-05T17:24:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/46/336d4e09a6af66532eedde5c8f03a73eaa91a046b408522259ab6a604363/onnxruntime_gpu-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:129f6ae8b331a6507759597cd317b23e94aed6ead1da951f803c3328f2990b0c", size = 209487551, upload-time = "2026-02-05T17:24:26.373Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" }, ] [[package]] @@ -1718,87 +1755,88 @@ wheels = [ [[package]] name = "opencv-python-headless" -version = "4.11.0.86" +version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460, upload-time = "2025-01-16T13:52:57.015Z" }, - { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330, upload-time = "2025-01-16T13:55:45.731Z" }, - { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload-time = "2025-01-16T13:51:59.625Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload-time = "2025-01-16T13:53:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload-time = "2025-01-16T13:52:49.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, ] [[package]] name = "orjson" -version = "3.11.5" +version = "3.11.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, - { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, - { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, - { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, - { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, - { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, - { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, - { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, - { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, - { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, - { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, - { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, - { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, - { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, - { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, - { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, - { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, - { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, - { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, - { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, - { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, - { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, - { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, - { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, - { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, - { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, - { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, - { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, - { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, - { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, - { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, - { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, - { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, - { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, - { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, - { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, ] [[package]] @@ -1821,52 +1859,89 @@ wheels = [ [[package]] name = "pillow" -version = "10.4.0" +version = "12.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, - { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, - { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload-time = "2024-07-01T09:45:58.437Z" }, - { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload-time = "2024-07-01T09:46:00.713Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload-time = "2024-07-01T09:46:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload-time = "2024-07-01T09:46:05.356Z" }, - { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload-time = "2024-07-01T09:46:08.145Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload-time = "2024-07-01T09:46:10.211Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload-time = "2024-07-01T09:46:12.685Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload-time = "2024-07-01T09:46:14.83Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" }, - { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" }, - { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" }, - { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" }, - { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" }, - { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" }, - { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" }, - { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" }, - { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" }, - { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" }, - { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] [[package]] @@ -1956,7 +2031,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.7" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1964,88 +2039,120 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] [[package]] name = "pydantic-settings" -version = "2.10.1" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] [[package]] @@ -2106,28 +2213,28 @@ wheels = [ [[package]] name = "pytest-cov" -version = "6.2.1" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] name = "pytest-mock" -version = "3.14.1" +version = "3.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] @@ -2165,11 +2272,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -2320,7 +2427,7 @@ wheels = [ [[package]] name = "rapidocr" -version = "3.4.5" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorlog" }, @@ -2336,7 +2443,7 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/be/5a/9a61f7c3250d7651c2043e763045e1181fe2fd12d0d5879f726f351818ad/rapidocr-3.4.5-py3-none-any.whl", hash = "sha256:6fb21ffb55b3aa49fee2a7c5cc5190851180e5be538b076b2166b7f44213cd5c", size = 15060573, upload-time = "2025-12-18T03:16:15.738Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/0d025466f0f84552634f2a94c018df34568fe55cc97184a6bb2c719c5b3a/rapidocr-3.6.0-py3-none-any.whl", hash = "sha256:d16b43872fc4dfa1e60996334dcd0dc3e3f1f64161e2332bc1873b9f65754e6b", size = 15067340, upload-time = "2026-01-28T14:45:04.271Z" }, ] [[package]] @@ -2356,15 +2463,15 @@ wheels = [ [[package]] name = "rich" -version = "14.1.0" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] @@ -2430,28 +2537,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.10" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, - { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, - { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, - { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, - { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, - { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, - { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, - { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, - { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, - { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] @@ -2717,27 +2823,28 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.21.4" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, - { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, - { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, - { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, - { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, - { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, - { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] [[package]] @@ -2803,32 +2910,32 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20250822" +version = "6.0.12.20250915" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/85/90a442e538359ab5c9e30de415006fb22567aa4301c908c09f19e42975c2/types_pyyaml-6.0.12.20250822.tar.gz", hash = "sha256:259f1d93079d335730a9db7cff2bcaf65d7e04b4a56b5927d49a612199b59413", size = 17481, upload-time = "2025-08-22T03:02:16.209Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/8e/8f0aca667c97c0d76024b37cffa39e76e2ce39ca54a38f285a64e6ae33ba/types_pyyaml-6.0.12.20250822-py3-none-any.whl", hash = "sha256:1fe1a5e146aa315483592d292b72a172b65b946a6d98aa6ddd8e4aa838ab7098", size = 20314, upload-time = "2025-08-22T03:02:15.002Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, ] [[package]] name = "types-requests" -version = "2.32.4.20250809" +version = "2.32.4.20260107" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, ] [[package]] name = "types-setuptools" -version = "80.9.0.20250822" +version = "82.0.0.20260210" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/bd/1e5f949b7cb740c9f0feaac430e301b8f1c5f11a81e26324299ea671a237/types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965", size = 41296, upload-time = "2025-08-22T03:02:08.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/90/796ac8c774a7f535084aacbaa6b7053d16fff5c630eff87c3ecff7896c37/types_setuptools-82.0.0.20260210.tar.gz", hash = "sha256:d9719fbbeb185254480ade1f25327c4654f8c00efda3fec36823379cebcdee58", size = 44768, upload-time = "2026-02-10T04:22:02.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/2d/475bf15c1cdc172e7a0d665b6e373ebfb1e9bf734d3f2f543d668b07a142/types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3", size = 63179, upload-time = "2025-08-22T03:02:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/3e/54/3489432b1d9bc713c9d8aa810296b8f5b0088403662959fb63a8acdbd4fc/types_setuptools-82.0.0.20260210-py3-none-any.whl", hash = "sha256:5124a7daf67f195c6054e0f00f1d97c69caad12fdcf9113eba33eff0bce8cd2b", size = 68433, upload-time = "2026-02-10T04:22:00.876Z" }, ] [[package]] @@ -2851,23 +2958,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] @@ -2881,15 +2988,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.35.0" +version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] [package.optional-dependencies] diff --git a/mise.toml b/mise.toml index 0e7237be20..3ca0d353ea 100644 --- a/mise.toml +++ b/mise.toml @@ -16,7 +16,7 @@ config_roots = [ [tools] node = "24.13.0" flutter = "3.35.7" -pnpm = "10.28.0" +pnpm = "10.28.2" terragrunt = "0.98.0" opentofu = "1.11.4" java = "21.0.2" diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index eacf75b7ed..db3859ab6e 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -117,7 +117,7 @@ android:pathPrefix="/albums/" /> + android:pathPrefix="/people/" /> diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index befc2c11c5..14a6b4b660 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 3036, - "android.injected.version.name" => "2.5.5", + "android.injected.version.code" => 3037, + "android.injected.version.name" => "2.5.6", } ) 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') diff --git a/mobile/bin/generate_keys.dart b/mobile/bin/generate_keys.dart index 8353b1c6f4..3c5c284c3e 100644 --- a/mobile/bin/generate_keys.dart +++ b/mobile/bin/generate_keys.dart @@ -3,7 +3,99 @@ import 'dart:convert'; import 'dart:io'; -const _kReservedWords = ['continue']; +const _kReservedWords = [ + 'abstract', + 'as', + 'assert', + 'async', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'covariant', + 'default', + 'deferred', + 'do', + 'dynamic', + 'else', + 'enum', + 'export', + 'extends', + 'extension', + 'external', + 'factory', + 'false', + 'final', + 'finally', + 'for', + 'Function', + 'get', + 'hide', + 'if', + 'implements', + 'import', + 'in', + 'interface', + 'is', + 'late', + 'library', + 'mixin', + 'new', + 'null', + 'on', + 'operator', + 'part', + 'required', + 'rethrow', + 'return', + 'sealed', + 'set', + 'show', + 'static', + 'super', + 'switch', + 'sync', + 'this', + 'throw', + 'true', + 'try', + 'typedef', + 'var', + 'void', + 'when', + 'while', + 'with', + 'yield', +]; + +const _kIntParamNames = [ + 'count', + 'number', + 'amount', + 'total', + 'index', + 'size', + 'length', + 'width', + 'height', + 'year', + 'month', + 'day', + 'hour', + 'minute', + 'second', + 'page', + 'limit', + 'offset', + 'max', + 'min', + 'id', + 'num', + 'quantity', +]; void main() async { final sourceFile = File('../i18n/en.json'); @@ -15,49 +107,258 @@ void main() async { final outputDir = Directory('lib/generated'); await outputDir.create(recursive: true); - final outputFile = File('lib/generated/intl_keys.g.dart'); - await _generate(sourceFile, outputFile); + final content = await sourceFile.readAsString(); + final translations = json.decode(content) as Map; + + final outputFile = File('lib/generated/translations.g.dart'); + await _generateTranslations(translations, outputFile); print('Generated ${outputFile.path}'); } -Future _generate(File source, File output) async { - final content = await source.readAsString(); - final translations = json.decode(content) as Map; +class TranslationNode { + final String key; + final String? value; + final Map children; + final List params; + + const TranslationNode({ + required this.key, + this.value, + Map? children, + List? params, + }) : children = children ?? const {}, + params = params ?? const []; + + bool get isLeaf => value != null; + bool get hasParams => params.isNotEmpty; +} + +class TranslationParam { + final String name; + final String type; + + const TranslationParam(this.name, this.type); +} + +Future _generateTranslations(Map translations, File output) async { + final root = _buildTranslationTree('', translations); final buffer = StringBuffer(''' // DO NOT EDIT. This is code generated via generate_keys.dart -abstract class IntlKeys { -'''); +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/message_format.dart'; - _writeKeys(buffer, translations); - buffer.writeln('}'); - - await output.writeAsString(buffer.toString()); +extension TranslationsExtension on BuildContext { + Translations get t => Translations.of(this); } -void _writeKeys( - StringBuffer buffer, - Map map, [ - String prefix = '', -]) { - for (final entry in map.entries) { - final key = entry.key; - final value = entry.value; +class StaticTranslations { + StaticTranslations._(); + static final instance = Translations._(null); +} - if (value is Map) { - _writeKeys(buffer, value, prefix.isEmpty ? key : '${prefix}_$key'); - } else { - final name = _cleanName(prefix.isEmpty ? key : '${prefix}_$key'); - final path = prefix.isEmpty ? key : '$prefix.$key'.replaceAll('_', '.'); - buffer.writeln(' static const $name = \'$path\';'); +abstract class _BaseTranslations { + BuildContext? get _context; + + String _t(String key, [Map? args]) { + if (key.isEmpty) return ''; + try { + final translated = key.tr(context: _context); + return args != null + ? MessageFormat(translated, locale: Intl.defaultLocale ?? 'en').format(args) + : translated; + } catch (e) { + return key; } } } -String _cleanName(String name) { - name = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); - if (RegExp(r'^[0-9]').hasMatch(name)) name = 'k_$name'; - if (_kReservedWords.contains(name)) name = '${name}_'; +class Translations extends _BaseTranslations { + @override + final BuildContext? _context; + Translations._(this._context); + + static Translations of(BuildContext context) { + context.locale; + return Translations._(context); + } + +'''); + + _generateClassMembers(buffer, root, ' '); + buffer.writeln('}'); + _generateNestedClasses(buffer, root); + + await output.writeAsString(buffer.toString()); +} + +TranslationNode _buildTranslationTree(String key, dynamic value) { + if (value is Map) { + final children = {}; + for (final entry in value.entries) { + children[entry.key] = _buildTranslationTree(entry.key, entry.value); + } + return TranslationNode(key: key, children: children); + } else { + final stringValue = value.toString(); + final params = _extractParams(stringValue); + return TranslationNode(key: key, value: stringValue, params: params); + } +} + +List _extractParams(String value) { + final params = {}; + + final icuRegex = RegExp(r'\{(\w+),\s*(plural|select|number|date|time)([^}]*(?:\{[^}]*\}[^}]*)*)\}'); + for (final match in icuRegex.allMatches(value)) { + final name = match.group(1)!; + final icuType = match.group(2)!; + final icuContent = match.group(3) ?? ''; + + if (params.containsKey(name)) continue; + + String type; + if (icuType == 'plural' || icuType == 'number') { + type = 'int'; + } else if (icuType == 'select') { + final hasTrueFalse = RegExp(r',\s*(true|false)\s*\{').hasMatch(icuContent); + type = hasTrueFalse ? 'bool' : 'String'; + } else { + type = 'String'; + } + + params[name] = TranslationParam(name, type); + } + + var cleanedValue = value; + var depth = 0; + var icuStart = -1; + + for (var i = 0; i < value.length; i++) { + if (value[i] == '{') { + if (depth == 0) icuStart = i; + depth++; + } else if (value[i] == '}') { + depth--; + if (depth == 0 && icuStart >= 0) { + final block = value.substring(icuStart, i + 1); + if (RegExp(r'^\{\w+,').hasMatch(block)) { + cleanedValue = cleanedValue.replaceFirst(block, ''); + } + icuStart = -1; + } + } + } + + final simpleRegex = RegExp(r'\{(\w+)\}'); + for (final match in simpleRegex.allMatches(cleanedValue)) { + final name = match.group(1)!; + + if (params.containsKey(name)) continue; + + String type; + if (_kIntParamNames.contains(name.toLowerCase())) { + type = 'int'; + } else { + type = 'Object'; + } + + params[name] = TranslationParam(name, type); + } + + return params.values.toList(); +} + +void _generateClassMembers(StringBuffer buffer, TranslationNode node, String indent, [String keyPrefix = '']) { + final sortedKeys = node.children.keys.toList()..sort(); + + for (final childKey in sortedKeys) { + final child = node.children[childKey]!; + final dartName = _escapeName(childKey); + final fullKey = keyPrefix.isEmpty ? childKey : '$keyPrefix.$childKey'; + + if (child.isLeaf) { + if (child.hasParams) { + _generateMethod(buffer, dartName, fullKey, child.params, indent); + } else { + _generateGetter(buffer, dartName, fullKey, indent); + } + } else { + final className = _toNestedClassName(keyPrefix, childKey); + buffer.writeln('${indent}late final $dartName = $className._(_context);'); + } + } +} + +void _generateGetter(StringBuffer buffer, String dartName, String translationKey, String indent) { + buffer.writeln('${indent}String get $dartName => _t(\'$translationKey\');'); +} + +void _generateMethod( + StringBuffer buffer, + String dartName, + String translationKey, + List params, + String indent, +) { + final paramList = params.map((p) => 'required ${p.type} ${_escapeName(p.name)}').join(', '); + final argsMap = params.map((p) => '\'${p.name}\': ${_escapeName(p.name)}').join(', '); + buffer.writeln('${indent}String $dartName({$paramList}) => _t(\'$translationKey\', {$argsMap});'); +} + +void _generateNestedClasses(StringBuffer buffer, TranslationNode node, [String keyPrefix = '']) { + final sortedKeys = node.children.keys.toList()..sort(); + + for (final childKey in sortedKeys) { + final child = node.children[childKey]!; + final fullKey = keyPrefix.isEmpty ? childKey : '$keyPrefix.$childKey'; + + if (!child.isLeaf && child.children.isNotEmpty) { + final className = _toNestedClassName(keyPrefix, childKey); + buffer.writeln(); + buffer.writeln('class $className extends _BaseTranslations {'); + buffer.writeln(' @override'); + buffer.writeln(' final BuildContext? _context;'); + buffer.writeln(' $className._(this._context);'); + _generateClassMembers(buffer, child, ' ', fullKey); + buffer.writeln('}'); + _generateNestedClasses(buffer, child, fullKey); + } + } +} + +String _toNestedClassName(String prefix, String key) { + final parts = []; + if (prefix.isNotEmpty) { + parts.addAll(prefix.split('.')); + } + parts.add(key); + + final result = StringBuffer('_'); + for (final part in parts) { + final words = part.split('_'); + for (final word in words) { + if (word.isNotEmpty) { + result.write(word[0].toUpperCase()); + if (word.length > 1) { + result.write(word.substring(1).toLowerCase()); + } + } + } + } + result.write('Translations'); + + return result.toString(); +} + +String _escapeName(String name) { + if (_kReservedWords.contains(name)) { + return '$name\$'; + } + if (RegExp(r'^[0-9]').hasMatch(name)) { + return 'k$name'; + } return name; } diff --git a/mobile/drift_schemas/main/drift_schema_v19.json b/mobile/drift_schemas/main/drift_schema_v19.json new file mode 100644 index 0000000000..405650a41f --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v19.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[6],"type":"index","data":{"on":6,"name":"idx_local_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_remote_album_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)","unique":false,"columns":[]}},{"id":9,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":10,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":11,"references":[2],"type":"index","data":{"on":2,"name":"idx_stack_primary_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)","unique":false,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":13,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":14,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":15,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":16,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_stack_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)","unique":false,"columns":[]}},{"id":17,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_day","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))","unique":false,"columns":[]}},{"id":18,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_month","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))","unique":false,"columns":[]}},{"id":19,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":20,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":21,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":22,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":23,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":24,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":25,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":26,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":27,"references":[1,26],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":28,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":29,"references":[1,28],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":30,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":31,"references":[],"type":"table","data":{"name":"trashed_local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":32,"references":[21],"type":"index","data":{"on":21,"name":"idx_partner_shared_with_id","sql":"CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)","unique":false,"columns":[]}},{"id":33,"references":[22],"type":"index","data":{"on":22,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":34,"references":[23],"type":"index","data":{"on":23,"name":"idx_remote_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":35,"references":[25],"type":"index","data":{"on":25,"name":"idx_remote_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)","unique":false,"columns":[]}},{"id":36,"references":[28],"type":"index","data":{"on":28,"name":"idx_person_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)","unique":false,"columns":[]}},{"id":37,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_person_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)","unique":false,"columns":[]}},{"id":38,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)","unique":false,"columns":[]}},{"id":39,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":40,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index e0f06612fa..1557d7f701 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -80,7 +80,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.5.5 + 2.5.6 CFBundleSignature ???? CFBundleURLTypes diff --git a/mobile/lib/domain/services/people.service.dart b/mobile/lib/domain/services/people.service.dart index d45f710d7b..ecfe83e5cb 100644 --- a/mobile/lib/domain/services/people.service.dart +++ b/mobile/lib/domain/services/people.service.dart @@ -10,6 +10,10 @@ class DriftPeopleService { const DriftPeopleService(this._repository, this._personApiRepository); + Future get(String personId) { + return _repository.get(personId); + } + Future> getAssetPeople(String assetId) { return _repository.getAssetPeople(assetId); } diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart index 5f793030c3..45a0b436bd 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -3,6 +3,8 @@ import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)') +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)') class AssetFaceEntity extends Table with DriftDefaultsMixin { const AssetFaceEntity(); diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart index 092fcc5859..7f2f3825e3 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart @@ -588,6 +588,10 @@ typedef $$AssetFaceEntityTableProcessedTableManager = i1.AssetFaceEntityData, i0.PrefetchHooks Function({bool assetId, bool personId}) >; +i0.Index get idxAssetFacePersonId => i0.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', +); class $AssetFaceEntityTable extends i2.AssetFaceEntity with i0.TableInfo<$AssetFaceEntityTable, i1.AssetFaceEntityData> { @@ -1207,3 +1211,8 @@ class AssetFaceEntityCompanion .toString(); } } + +i0.Index get idxAssetFaceAssetId => i0.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', +); diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart index 53f1a10662..b0f4b1b27f 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -3,6 +3,9 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql( + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', +) class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { const LocalAlbumAssetEntity(); diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart index 70c298332b..77b2798afb 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -459,6 +459,10 @@ typedef $$LocalAlbumAssetEntityTableProcessedTableManager = i1.LocalAlbumAssetEntityData, i0.PrefetchHooks Function({bool assetId, bool albumId}) >; +i0.Index get idxLocalAlbumAssetAlbumAsset => i0.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', +); class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity with diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift index 217a18b75c..2e08b6424a 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -90,8 +90,14 @@ FROM ( SELECT CASE - WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', rae.local_date_time) - WHEN :group_by = 1 THEN STRFTIME('%Y-%m', rae.local_date_time) + WHEN :group_by = 0 THEN COALESCE( + STRFTIME('%Y-%m-%d', rae.local_date_time), + STRFTIME('%Y-%m-%d', rae.created_at, 'localtime') + ) + WHEN :group_by = 1 THEN COALESCE( + STRFTIME('%Y-%m', rae.local_date_time), + STRFTIME('%Y-%m', rae.created_at, 'localtime') + ) END as bucket_date FROM remote_asset_entity rae diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift.dart b/mobile/lib/infrastructure/entities/merged_asset.drift.dart index cd2fe5cfcc..e88d11d442 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift.dart +++ b/mobile/lib/infrastructure/entities/merged_asset.drift.dart @@ -79,7 +79,7 @@ class MergedAssetDrift extends i1.ModularAccessor { final expandeduserIds = $expandVar($arrayStartIndex, userIds.length); $arrayStartIndex += userIds.length; return customSelect( - 'SELECT COUNT(*) AS asset_count, bucket_date FROM (SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', rae.local_date_time) WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', rae.local_date_time) END AS bucket_date FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', lae.created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', lae.created_at, \'localtime\') END AS bucket_date FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC', + 'SELECT COUNT(*) AS asset_count, bucket_date FROM (SELECT CASE WHEN ?1 = 0 THEN COALESCE(STRFTIME(\'%Y-%m-%d\', rae.local_date_time), STRFTIME(\'%Y-%m-%d\', rae.created_at, \'localtime\')) WHEN ?1 = 1 THEN COALESCE(STRFTIME(\'%Y-%m\', rae.local_date_time), STRFTIME(\'%Y-%m\', rae.created_at, \'localtime\')) END AS bucket_date FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', lae.created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', lae.created_at, \'localtime\') END AS bucket_date FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC', variables: [ i0.Variable(groupBy), for (var $ in userIds) i0.Variable($), diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart index dbc675ee99..1d8dc6d87c 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)') class PartnerEntity extends Table with DriftDefaultsMixin { const PartnerEntity(); diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart index 01ec72fe23..76a91f27bf 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.drift.dart @@ -440,6 +440,10 @@ typedef $$PartnerEntityTableProcessedTableManager = i1.PartnerEntityData, i0.PrefetchHooks Function({bool sharedById, bool sharedWithId}) >; +i0.Index get idxPartnerSharedWithId => i0.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', +); class $PartnerEntityTable extends i2.PartnerEntity with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> { diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart index f0878e00f8..6e014590ab 100644 --- a/mobile/lib/infrastructure/entities/person.entity.dart +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)') class PersonEntity extends Table with DriftDefaultsMixin { const PersonEntity(); diff --git a/mobile/lib/infrastructure/entities/person.entity.drift.dart b/mobile/lib/infrastructure/entities/person.entity.drift.dart index ffbd796f4b..02ea48c846 100644 --- a/mobile/lib/infrastructure/entities/person.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/person.entity.drift.dart @@ -455,6 +455,10 @@ typedef $$PersonEntityTableProcessedTableManager = i1.PersonEntityData, i0.PrefetchHooks Function({bool ownerId}) >; +i0.Index get idxPersonOwnerId => i0.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', +); class $PersonEntityTable extends i2.PersonEntity with i0.TableInfo<$PersonEntityTable, i1.PersonEntityData> { diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart index 74b00dd9ee..30e13853d8 100644 --- a/mobile/lib/infrastructure/entities/remote_album.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)') class RemoteAlbumEntity extends Table with DriftDefaultsMixin { const RemoteAlbumEntity(); diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart index 30a6d0b535..7dc864b978 100644 --- a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart @@ -566,6 +566,10 @@ typedef $$RemoteAlbumEntityTableProcessedTableManager = i1.RemoteAlbumEntityData, i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId}) >; +i0.Index get idxRemoteAlbumOwnerId => i0.Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', +); class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity with i0.TableInfo<$RemoteAlbumEntityTable, i1.RemoteAlbumEntityData> { diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart index e99f5364a4..6d1e88514b 100644 --- a/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart @@ -3,6 +3,9 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql( + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', +) class RemoteAlbumAssetEntity extends Table with DriftDefaultsMixin { const RemoteAlbumAssetEntity(); diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart index adf22635c1..a03c4d7e96 100644 --- a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart @@ -441,6 +441,10 @@ typedef $$RemoteAlbumAssetEntityTableProcessedTableManager = i1.RemoteAlbumAssetEntityData, i0.PrefetchHooks Function({bool assetId, bool albumId}) >; +i0.Index get idxRemoteAlbumAssetAlbumAsset => i0.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', +); class $RemoteAlbumAssetEntityTable extends i2.RemoteAlbumAssetEntity with diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 4dc0fa568f..4c8b563616 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -19,6 +19,13 @@ ON remote_asset_entity (owner_id, library_id, checksum) WHERE (library_id IS NOT NULL); ''') @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)') +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)') +@TableIndex.sql( + "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))", +) +@TableIndex.sql( + "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))", +) class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const RemoteAssetEntity(); diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart index 2d9e8b235e..8231cfcd8a 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -1710,3 +1710,15 @@ i0.Index get idxRemoteAssetChecksum => i0.Index( 'idx_remote_asset_checksum', 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', ); +i0.Index get idxRemoteAssetStackId => i0.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', +); +i0.Index get idxRemoteAssetLocalDateTimeDay => i0.Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', +); +i0.Index get idxRemoteAssetLocalDateTimeMonth => i0.Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', +); diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart index be50d7e330..4f90845a45 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)') class StackEntity extends Table with DriftDefaultsMixin { const StackEntity(); diff --git a/mobile/lib/infrastructure/entities/stack.entity.drift.dart b/mobile/lib/infrastructure/entities/stack.entity.drift.dart index ff7a3c3444..55017f8344 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.drift.dart @@ -357,6 +357,10 @@ typedef $$StackEntityTableProcessedTableManager = i1.StackEntityData, i0.PrefetchHooks Function({bool ownerId}) >; +i0.Index get idxStackPrimaryAssetId => i0.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', +); class $StackEntityTable extends i2.StackEntity with i0.TableInfo<$StackEntityTable, i1.StackEntityData> { diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 652e9de943..5495d21bd3 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository { } @override - int get schemaVersion => 18; + int get schemaVersion => 19; @override MigrationStrategy get migration => MigrationStrategy( @@ -213,6 +213,19 @@ class Drift extends $Drift implements IDatabaseRepository { from17To18: (m, v18) async { await m.createIndex(v18.idxRemoteAssetCloudId); }, + from18To19: (m, v19) async { + await m.createIndex(v19.idxAssetFacePersonId); + await m.createIndex(v19.idxAssetFaceAssetId); + await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset); + await m.createIndex(v19.idxPartnerSharedWithId); + await m.createIndex(v19.idxPersonOwnerId); + await m.createIndex(v19.idxRemoteAlbumOwnerId); + await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset); + await m.createIndex(v19.idxRemoteAssetStackId); + await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay); + await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth); + await m.createIndex(v19.idxStackPrimaryAssetId); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index c561eef0c6..ae805ad25e 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -100,12 +100,18 @@ abstract class $Drift extends i0.GeneratedDatabase { remoteAlbumEntity, localAlbumEntity, localAlbumAssetEntity, + i7.idxLocalAlbumAssetAlbumAsset, + i5.idxRemoteAlbumOwnerId, i4.idxLocalAssetChecksum, i4.idxLocalAssetCloudId, + i3.idxStackPrimaryAssetId, i2.idxRemoteAssetOwnerChecksum, i2.uQRemoteAssetsOwnerChecksum, i2.uQRemoteAssetsOwnerLibraryChecksum, i2.idxRemoteAssetChecksum, + i2.idxRemoteAssetStackId, + i2.idxRemoteAssetLocalDateTimeDay, + i2.idxRemoteAssetLocalDateTimeMonth, authUserEntity, userMetadataEntity, partnerEntity, @@ -119,8 +125,13 @@ abstract class $Drift extends i0.GeneratedDatabase { assetFaceEntity, storeEntity, trashedLocalAssetEntity, + i10.idxPartnerSharedWithId, i11.idxLatLng, + i12.idxRemoteAlbumAssetAlbumAsset, i14.idxRemoteAssetCloudId, + i17.idxPersonOwnerId, + i18.idxAssetFacePersonId, + i18.idxAssetFaceAssetId, i20.idxTrashedLocalAssetChecksum, i20.idxTrashedLocalAssetAlbum, ]; diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 72601f249f..e56eb97c75 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -7857,6 +7857,509 @@ final class Schema18 extends i0.VersionedSchema { ); } +final class Schema19 extends i0.VersionedSchema { + Schema19({required super.database}) : super(version: 19); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 localAssetEntity = Shape26( + source: i0.VersionedTable( + entityName: 'local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_22, + _column_14, + _column_23, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAlbumOwnerId = i1.Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -7875,6 +8378,7 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema16 schema) from15To16, required Future Function(i1.Migrator m, Schema17 schema) from16To17, required Future Function(i1.Migrator m, Schema18 schema) from17To18, + required Future Function(i1.Migrator m, Schema19 schema) from18To19, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -7963,6 +8467,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from17To18(migrator, schema); return 18; + case 18: + final schema = Schema19(database: database); + final migrator = i1.Migrator(database, schema); + await from18To19(migrator, schema); + return 19; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -7987,6 +8496,7 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema16 schema) from15To16, required Future Function(i1.Migrator m, Schema17 schema) from16To17, required Future Function(i1.Migrator m, Schema18 schema) from17To18, + required Future Function(i1.Migrator m, Schema19 schema) from18To19, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -8006,5 +8516,6 @@ i1.OnUpgrade stepByStep({ from15To16: from15To16, from16To17: from16To17, from17To18: from17To18, + from18To19: from18To19, ), ); diff --git a/mobile/lib/infrastructure/repositories/people.repository.dart b/mobile/lib/infrastructure/repositories/people.repository.dart index e2b8646dba..40402b6f72 100644 --- a/mobile/lib/infrastructure/repositories/people.repository.dart +++ b/mobile/lib/infrastructure/repositories/people.repository.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -7,6 +8,13 @@ class DriftPeopleRepository extends DriftDatabaseRepository { final Drift _db; const DriftPeopleRepository(this._db) : super(_db); + Future get(String personId) async { + final query = _db.select(_db.personEntity)..where((row) => row.id.equals(personId)); + + final result = await query.getSingleOrNull(); + return result?.toDto(); + } + Future> getAssetPeople(String assetId) async { final query = _db.select(_db.assetFaceEntity).join([ innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)), @@ -19,19 +27,28 @@ class DriftPeopleRepository extends DriftDatabaseRepository { } Future> getAllPeople() async { + final people = _db.personEntity; + final faces = _db.assetFaceEntity; + final assets = _db.remoteAssetEntity; + final query = - _db.select(_db.personEntity).join([ - leftOuterJoin(_db.assetFaceEntity, _db.assetFaceEntity.personId.equalsExp(_db.personEntity.id)), + _db.select(people).join([ + innerJoin(faces, faces.personId.equalsExp(people.id)), + innerJoin(assets, assets.id.equalsExp(faces.assetId)), ]) - ..where(_db.personEntity.isHidden.equals(false)) - ..groupBy([_db.personEntity.id], having: _db.assetFaceEntity.id.count().isBiggerOrEqualValue(3)) + ..where( + people.isHidden.equals(false) & + assets.deletedAt.isNull() & + assets.visibility.equalsValue(AssetVisibility.timeline), + ) + ..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not()) ..orderBy([ - OrderingTerm(expression: _db.personEntity.name.equals('').not(), mode: OrderingMode.desc), - OrderingTerm(expression: _db.assetFaceEntity.id.count(), mode: OrderingMode.desc), + OrderingTerm(expression: people.name.equals('').not(), mode: OrderingMode.desc), + OrderingTerm(expression: faces.id.count(), mode: OrderingMode.desc), ]); return query.map((row) { - final person = row.readTable(_db.personEntity); + final person = row.readTable(people); return person.toDto(); }).get(); } diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 0e145395df..7544b4b2ac 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -203,7 +203,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { final album = albums.first; final isAscending = album.order == AlbumAssetOrder.asc; final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.localDateTime.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -361,7 +361,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.localDateTime.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -431,7 +431,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.localDateTime.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -501,7 +501,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.localDateTime.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -603,7 +603,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.localDateTime.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -678,6 +678,11 @@ extension on Expression { } } +extension on $RemoteAssetEntityTable { + Expression effectiveCreatedAt(GroupAssetsBy groupBy) => + coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]); +} + extension on String { DateTime truncateDate(GroupAssetsBy groupBy) { final format = switch (groupBy) { diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 86b1c6cc5f..1316e66273 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -18,7 +18,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; @@ -219,8 +219,8 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve ref .read(backgroundWorkerFgServiceProvider) .saveNotificationMessage( - IntlKeys.uploading_media.t(), - IntlKeys.backup_background_service_default_notification.t(), + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, ); } } else { diff --git a/mobile/lib/models/server_info/server_features.model.dart b/mobile/lib/models/server_info/server_features.model.dart index 049628a8d2..78a80c9013 100644 --- a/mobile/lib/models/server_info/server_features.model.dart +++ b/mobile/lib/models/server_info/server_features.model.dart @@ -6,6 +6,7 @@ class ServerFeatures { final bool oauthEnabled; final bool passwordLogin; final bool ocr; + final bool smartSearch; const ServerFeatures({ required this.trash, @@ -13,21 +14,30 @@ class ServerFeatures { required this.oauthEnabled, required this.passwordLogin, this.ocr = false, + this.smartSearch = false, }); - ServerFeatures copyWith({bool? trash, bool? map, bool? oauthEnabled, bool? passwordLogin, bool? ocr}) { + ServerFeatures copyWith({ + bool? trash, + bool? map, + bool? oauthEnabled, + bool? passwordLogin, + bool? ocr, + bool? smartSearch, + }) { return ServerFeatures( trash: trash ?? this.trash, map: map ?? this.map, oauthEnabled: oauthEnabled ?? this.oauthEnabled, passwordLogin: passwordLogin ?? this.passwordLogin, ocr: ocr ?? this.ocr, + smartSearch: smartSearch ?? this.smartSearch, ); } @override String toString() { - return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr)'; + return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch)'; } ServerFeatures.fromDto(ServerFeaturesDto dto) @@ -35,7 +45,8 @@ class ServerFeatures { map = dto.map, oauthEnabled = dto.oauth, passwordLogin = dto.passwordLogin, - ocr = dto.ocr; + ocr = dto.ocr, + smartSearch = dto.smartSearch; @override bool operator ==(covariant ServerFeatures other) { @@ -45,11 +56,17 @@ class ServerFeatures { other.map == map && other.oauthEnabled == oauthEnabled && other.passwordLogin == passwordLogin && - other.ocr == ocr; + other.ocr == ocr && + other.smartSearch == smartSearch; } @override int get hashCode { - return trash.hashCode ^ map.hashCode ^ oauthEnabled.hashCode ^ passwordLogin.hashCode ^ ocr.hashCode; + return trash.hashCode ^ + map.hashCode ^ + oauthEnabled.hashCode ^ + passwordLogin.hashCode ^ + ocr.hashCode ^ + smartSearch.hashCode; } } diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index 5d78acb0b8..a039bb70eb 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -28,7 +28,7 @@ class ServerInfo { const ServerInfo({ required this.serverVersion, - required this.latestVersion, + this.latestVersion, required this.serverFeatures, required this.serverConfig, required this.serverDiskInfo, diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart index b0f682ffed..ca65a92a79 100644 --- a/mobile/lib/pages/album/album_options.page.dart +++ b/mobile/lib/pages/album/album_options.page.dart @@ -134,7 +134,7 @@ class AlbumOptionsPage extends HookConsumerWidget { itemBuilder: (context, index) { final user = sharedUsers.value[index]; return ListTile( - leading: UserCircleAvatar(user: user, radius: 22), + leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart index fe1823ec61..7cf6f387ae 100644 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ b/mobile/lib/pages/album/album_shared_user_icons.dart @@ -41,7 +41,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget { itemBuilder: ((context, index) { return Padding( padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar(user: sharedUsers.value[index], radius: 18, size: 36), + child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), ); }), itemCount: sharedUsers.value.length, diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 440544f989..cd6c2a62b0 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; @@ -153,7 +153,7 @@ class _DriftBackupPageState extends ConsumerState { Icon(Icons.warning_rounded, color: context.colorScheme.error, fill: 1), const SizedBox(width: 8), Text( - IntlKeys.backup_error_sync_failed.t(), + context.t.backup_error_sync_failed, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error), textAlign: TextAlign.center, ), diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index 1cfab355d6..c7c34b9cd2 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -7,7 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class SettingsHeader { String key = ""; @@ -61,7 +61,7 @@ class HeaderSettingsPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text(IntlKeys.headers_settings_tile_title).tr(), + title: Text(context.t.headers_settings_tile_title), centerTitle: false, actions: [ IconButton( diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 6332a662b9..99a534e9cf 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/partner.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; @@ -41,13 +41,13 @@ class LibraryPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const FavoritesRoute()), icon: Icons.favorite_outline_rounded, - label: IntlKeys.favorites.tr(), + label: context.t.favorites, ), const SizedBox(width: 8), ActionButton( onPressed: () => context.pushRoute(const ArchiveRoute()), icon: Icons.archive_outlined, - label: IntlKeys.archived.tr(), + label: context.t.archived, ), ], ), @@ -58,14 +58,14 @@ class LibraryPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const SharedLinkRoute()), icon: Icons.link_outlined, - label: IntlKeys.shared_links.tr(), + label: context.t.shared_links, ), SizedBox(width: trashEnabled ? 8 : 0), trashEnabled ? ActionButton( onPressed: () => context.pushRoute(const TrashRoute()), icon: Icons.delete_outline_rounded, - label: IntlKeys.trash.tr(), + label: context.t.trash, ) : const SizedBox.shrink(), ], @@ -120,26 +120,20 @@ class QuickAccessButtons extends ConsumerWidget { ), ), leading: const Icon(Icons.folder_outlined, size: 26), - title: Text( - IntlKeys.folders.tr(), - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), + title: Text(context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), onTap: () => context.pushRoute(FolderRoute()), ), ListTile( leading: const Icon(Icons.lock_outline_rounded, size: 26), title: Text( - IntlKeys.locked_folder.tr(), + context.t.locked_folder, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(const LockedRoute()), ), ListTile( leading: const Icon(Icons.group_outlined, size: 26), - title: Text( - IntlKeys.partners.tr(), - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), + title: Text(context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), onTap: () => context.pushRoute(const PartnerRoute()), ), PartnerList(partners: partners), @@ -230,7 +224,7 @@ class PeopleCollectionCard extends ConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - IntlKeys.people.tr(), + context.t.people, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -290,7 +284,7 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - IntlKeys.on_this_device.tr(), + context.t.on_this_device, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -341,7 +335,7 @@ class PlacesCollectionCard extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - IntlKeys.places.tr(), + context.t.places, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index cde8c127db..c9fed636b4 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -44,8 +44,8 @@ class _DriftAlbumsPageState extends ConsumerState { pinned: true, actions: [ IconButton( - icon: const Icon(Icons.add_rounded, size: 28), onPressed: () => context.pushRoute(const DriftCreateAlbumRoute()), + icon: const Icon(Icons.add_rounded), ), ], showUploadButton: false, diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 9db6e98613..061edbaf26 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -149,7 +149,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { } return ListTile( - leading: UserCircleAvatar(user: user, radius: 22), + leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context), @@ -169,7 +169,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { itemBuilder: (context, index) { final user = sharedUsers[index]; return ListTile( - leading: UserCircleAvatar(user: user, radius: 22), + leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart index 8713166027..a85f69a75e 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -43,9 +44,7 @@ class DriftTrashPage extends StatelessWidget { return SliverPadding( padding: const EdgeInsets.all(16.0), - sliver: SliverToBoxAdapter( - child: const Text("trash_page_info").t(context: context, args: {"days": "$trashDays"}), - ), + sliver: SliverToBoxAdapter(child: Text(context.t.trash_page_info(days: trashDays))), ); }, ), diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 16655e98f6..62ec11f7ed 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/feature_check.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -39,8 +40,15 @@ class DriftSearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final textSearchType = useState(TextSearchType.context); - final searchHintText = useState('sunrise_on_the_beach'.t(context: context)); + final serverFeatures = ref.watch(serverInfoProvider.select((v) => v.serverFeatures)); + final textSearchType = useState( + serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename, + ); + final searchHintText = useState( + serverFeatures.smartSearch + ? 'sunrise_on_the_beach'.t(context: context) + : 'file_name_or_extension'.t(context: context), + ); final textSearchController = useTextEditingController(); final preFilter = ref.watch(searchPreFilterProvider); final filter = useState( @@ -518,23 +526,26 @@ class DriftSearchPage extends HookConsumerWidget { ); }, menuChildren: [ - MenuItemButton( - child: ListTile( - leading: const Icon(Icons.image_search_rounded), - title: Text( - 'search_by_context'.t(context: context), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, + FeatureCheck( + feature: (features) => features.smartSearch, + child: MenuItemButton( + child: ListTile( + leading: const Icon(Icons.image_search_rounded), + title: Text( + 'search_by_context'.t(context: context), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, + ), ), + selectedColor: context.colorScheme.primary, + selected: textSearchType.value == TextSearchType.context, ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, + onPressed: () { + textSearchType.value = TextSearchType.context; + searchHintText.value = 'sunrise_on_the_beach'.t(context: context); + }, ), - onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.t(context: context); - }, ), MenuItemButton( child: ListTile( diff --git a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart index fe5c763ec5..691b46f80d 100644 --- a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart +++ b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart @@ -88,7 +88,7 @@ class _DriftActivityTextFieldState extends ConsumerState prefixIcon: user != null ? Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30, radius: 15), + child: UserCircleAvatar(user: user, size: 30), ) : null, suffixIcon: IconButton( diff --git a/mobile/lib/presentation/widgets/timeline/fixed/row.dart b/mobile/lib/presentation/widgets/timeline/fixed/row.dart index 3fe3cea3c9..97067add24 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/row.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/row.dart @@ -1,27 +1,45 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -class FixedTimelineRow extends MultiChildRenderObjectWidget { - final double dimension; +class TimelineRow extends MultiChildRenderObjectWidget { + final double height; + final List widths; final double spacing; final TextDirection textDirection; - const FixedTimelineRow({ + const TimelineRow({ super.key, - required this.dimension, + required this.height, + required this.widths, required this.spacing, required this.textDirection, required super.children, }); + factory TimelineRow.fixed({ + required double dimension, + required double spacing, + required TextDirection textDirection, + required List children, + }) => TimelineRow( + height: dimension, + widths: List.filled(children.length, dimension), + spacing: spacing, + textDirection: textDirection, + children: children, + ); + @override RenderObject createRenderObject(BuildContext context) { - return RenderFixedRow(dimension: dimension, spacing: spacing, textDirection: textDirection); + return RenderFixedRow(height: height, widths: widths, spacing: spacing, textDirection: textDirection); } @override void updateRenderObject(BuildContext context, RenderFixedRow renderObject) { - renderObject.dimension = dimension; + renderObject.height = height; + renderObject.widths = widths; renderObject.spacing = spacing; renderObject.textDirection = textDirection; } @@ -29,7 +47,8 @@ class FixedTimelineRow extends MultiChildRenderObjectWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DoubleProperty('dimension', dimension)); + properties.add(DoubleProperty('height', height)); + properties.add(DiagnosticsProperty>('widths', widths)); properties.add(DoubleProperty('spacing', spacing)); properties.add(EnumProperty('textDirection', textDirection)); } @@ -43,21 +62,32 @@ class RenderFixedRow extends RenderBox RenderBoxContainerDefaultsMixin { RenderFixedRow({ List? children, - required double dimension, + required double height, + required List widths, required double spacing, required TextDirection textDirection, - }) : _dimension = dimension, + }) : _height = height, + _widths = widths, _spacing = spacing, _textDirection = textDirection { addAll(children); } - double get dimension => _dimension; - double _dimension; + double get height => _height; + double _height; - set dimension(double value) { - if (_dimension == value) return; - _dimension = value; + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsLayout(); + } + + List get widths => _widths; + List _widths; + + set widths(List value) { + if (listEquals(_widths, value)) return; + _widths = value; markNeedsLayout(); } @@ -86,7 +116,7 @@ class RenderFixedRow extends RenderBox } } - double get intrinsicWidth => dimension * childCount + spacing * (childCount - 1); + double get intrinsicWidth => widths.sum + (spacing * (childCount - 1)); @override double computeMinIntrinsicWidth(double height) => intrinsicWidth; @@ -95,10 +125,10 @@ class RenderFixedRow extends RenderBox double computeMaxIntrinsicWidth(double height) => intrinsicWidth; @override - double computeMinIntrinsicHeight(double width) => dimension; + double computeMinIntrinsicHeight(double width) => height; @override - double computeMaxIntrinsicHeight(double width) => dimension; + double computeMaxIntrinsicHeight(double width) => height; @override double? computeDistanceToActualBaseline(TextBaseline baseline) { @@ -118,7 +148,8 @@ class RenderFixedRow extends RenderBox @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DoubleProperty('dimension', dimension)); + properties.add(DoubleProperty('height', height)); + properties.add(DiagnosticsProperty>('widths', widths)); properties.add(DoubleProperty('spacing', spacing)); properties.add(EnumProperty('textDirection', textDirection)); } @@ -131,19 +162,25 @@ class RenderFixedRow extends RenderBox return; } // Use the entire width of the parent for the row. - size = Size(constraints.maxWidth, dimension); - // Each tile is forced to be dimension x dimension. - final childConstraints = BoxConstraints.tight(Size(dimension, dimension)); + size = Size(constraints.maxWidth, height); + final flipMainAxis = textDirection == TextDirection.rtl; - Offset offset = Offset(flipMainAxis ? size.width - dimension : 0, 0); - final dx = (flipMainAxis ? -1 : 1) * (dimension + spacing); + int childIndex = 0; + double currentX = flipMainAxis ? size.width - (widths.firstOrNull ?? 0) : 0; // Layout each child horizontally. - while (child != null) { + while (child != null && childIndex < widths.length) { + final width = widths[childIndex]; + final childConstraints = BoxConstraints.tight(Size(width, height)); child.layout(childConstraints, parentUsesSize: false); final childParentData = child.parentData! as _RowParentData; - childParentData.offset = offset; - offset += Offset(dx, 0); + childParentData.offset = Offset(currentX, 0); child = childParentData.nextSibling; + childIndex++; + + if (child != null && childIndex < widths.length) { + final nextWidth = widths[childIndex]; + currentX += flipMainAxis ? -(spacing + nextWidth) : width + spacing; + } } } } diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index b879b33f68..aa2112b8dd 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart'; @@ -78,6 +80,7 @@ class FixedSegment extends Segment { assetCount: numberOfAssets, tileHeight: tileHeight, spacing: spacing, + columnCount: columnCount, ); } } @@ -87,24 +90,32 @@ class _FixedSegmentRow extends ConsumerWidget { final int assetCount; final double tileHeight; final double spacing; + final int columnCount; const _FixedSegmentRow({ required this.assetIndex, required this.assetCount, required this.tileHeight, required this.spacing, + required this.columnCount, }); @override Widget build(BuildContext context, WidgetRef ref) { final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing)); final timelineService = ref.read(timelineServiceProvider); + final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3); if (isScrubbing) { return _buildPlaceholder(context); } if (timelineService.hasRange(assetIndex, assetCount)) { - return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService); + return _buildAssetRow( + context, + timelineService.getAssets(assetIndex, assetCount), + timelineService, + isDynamicLayout, + ); } return FutureBuilder>( @@ -113,7 +124,7 @@ class _FixedSegmentRow extends ConsumerWidget { if (snapshot.connectionState != ConnectionState.done) { return _buildPlaceholder(context); } - return _buildAssetRow(context, snapshot.requireData, timelineService); + return _buildAssetRow(context, snapshot.requireData, timelineService, isDynamicLayout); }, ); } @@ -122,23 +133,58 @@ class _FixedSegmentRow extends ConsumerWidget { return SegmentBuilder.buildPlaceholder(context, assetCount, size: Size.square(tileHeight), spacing: spacing); } - Widget _buildAssetRow(BuildContext context, List assets, TimelineService timelineService) { - return FixedTimelineRow( - dimension: tileHeight, - spacing: spacing, - textDirection: Directionality.of(context), - children: [ - for (int i = 0; i < assets.length; i++) - TimelineAssetIndexWrapper( + Widget _buildAssetRow( + BuildContext context, + List assets, + TimelineService timelineService, + bool isDynamicLayout, + ) { + final children = [ + for (int i = 0; i < assets.length; i++) + TimelineAssetIndexWrapper( + assetIndex: assetIndex + i, + segmentIndex: 0, // For simplicity, using 0 for now + child: _AssetTileWidget( + key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), + asset: assets[i], assetIndex: assetIndex + i, - segmentIndex: 0, // For simplicity, using 0 for now - child: _AssetTileWidget( - key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), - asset: assets[i], - assetIndex: assetIndex + i, - ), ), - ], + ), + ]; + + final widths = List.filled(assets.length, tileHeight); + + if (isDynamicLayout) { + final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); + final meanAspectRatio = aspectRatios.sum / assets.length; + + // 1: mean width + // 0.5: width < mean - threshold + // 1.5: width > mean + threshold + final arConfiguration = aspectRatios.map((e) { + if (e - meanAspectRatio > 0.3) return 1.5; + if (e - meanAspectRatio < -0.3) return 0.5; + return 1.0; + }); + + // Normalize to get width distribution + final sum = arConfiguration.sum; + + int index = 0; + for (final ratio in arConfiguration) { + // Distribute the available width proportionally based on aspect ratio configuration + widths[index++] = ((ratio * assets.length) / sum) * tileHeight; + } + } + + return TimelineDragRegion( + child: TimelineRow( + height: tileHeight, + widths: widths, + spacing: spacing, + textDirection: Directionality.of(context), + children: children, + ), ); } } diff --git a/mobile/lib/presentation/widgets/timeline/segment_builder.dart b/mobile/lib/presentation/widgets/timeline/segment_builder.dart index 79ffb47e95..442d42d536 100644 --- a/mobile/lib/presentation/widgets/timeline/segment_builder.dart +++ b/mobile/lib/presentation/widgets/timeline/segment_builder.dart @@ -24,7 +24,7 @@ abstract class SegmentBuilder { Size size = kTimelineFixedTileExtent, double spacing = kTimelineSpacing, }) => RepaintBoundary( - child: FixedTimelineRow( + child: TimelineRow.fixed( dimension: size.height, spacing: spacing, textDirection: Directionality.of(context), diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index ac20e73190..da0497539b 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -29,7 +29,7 @@ import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart'; -class Timeline extends StatelessWidget { +class Timeline extends ConsumerWidget { const Timeline({ super.key, this.topSliverWidget, @@ -58,15 +58,15 @@ class Timeline extends StatelessWidget { final bool readOnly; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: const DownloadStatusFloatingButton(), body: LayoutBuilder( builder: (_, constraints) => ProviderScope( overrides: [ - timelineArgsProvider.overrideWith( - (ref) => TimelineArgs( + timelineArgsProvider.overrideWithValue( + TimelineArgs( maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight, columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))), @@ -78,6 +78,7 @@ class Timeline extends StatelessWidget { if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()), ], child: _SliverTimeline( + key: const ValueKey('_sliver_timeline'), topSliverWidget: topSliverWidget, topSliverWidgetHeight: topSliverWidgetHeight, appBar: appBar, @@ -105,6 +106,7 @@ class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier { class _SliverTimeline extends ConsumerStatefulWidget { const _SliverTimeline({ + super.key, this.topSliverWidget, this.topSliverWidgetHeight, this.appBar, @@ -139,14 +141,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { int _perRow = 4; double _scaleFactor = 3.0; double _baseScaleFactor = 3.0; - int? _scaleRestoreAssetIndex; + int? _restoreAssetIndex; @override void initState() { super.initState(); _scrollController = ScrollController( initialScrollOffset: widget.initialScrollOffset ?? 0.0, - onAttach: _restoreScalePosition, + onAttach: _restoreAssetPosition, ); _eventSubscription = EventStream.shared.listen(_onEvent); @@ -179,14 +181,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); } - void _restoreScalePosition(_) { - if (_scaleRestoreAssetIndex == null) return; + void _restoreAssetPosition(_) { + if (_restoreAssetIndex == null) return; final asyncSegments = ref.read(timelineSegmentProvider); asyncSegments.whenData((segments) { - final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _scaleRestoreAssetIndex!); + final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!); if (targetSegment != null) { - final assetIndexInSegment = _scaleRestoreAssetIndex! - targetSegment.firstAssetIndex; + final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex; final newColumnCount = ref.read(timelineArgsProvider).columnCount; final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; @@ -198,7 +200,25 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { }); } }); - _scaleRestoreAssetIndex = null; + _restoreAssetIndex = null; + } + + int? _getCurrentAssetIndex(List segments) { + final currentOffset = _scrollController.offset.clamp(0.0, _scrollController.position.maxScrollExtent); + final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull; + int? targetAssetIndex; + if (segment != null) { + final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset); + if (rowIndex > segment.firstIndex) { + final rowIndexInSegment = rowIndex - (segment.firstIndex + 1); + final assetsPerRow = ref.read(timelineArgsProvider).columnCount; + final assetIndexInSegment = rowIndexInSegment * assetsPerRow; + targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment; + } else { + targetAssetIndex = segment.firstAssetIndex; + } + } + return targetAssetIndex; } @override @@ -387,74 +407,66 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { return PrimaryScrollController( controller: _scrollController, - child: RawGestureDetector( - gestures: { - CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomScaleGestureRecognizer(), - (CustomScaleGestureRecognizer scale) { - scale.onStart = (details) { - _baseScaleFactor = _scaleFactor; - }; - - scale.onUpdate = (details) { - final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0); - final newPerRow = 7 - newScaleFactor.toInt(); - - if (newPerRow != _perRow) { - final currentOffset = _scrollController.offset.clamp( - 0.0, - _scrollController.position.maxScrollExtent, - ); - final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull; - int? targetAssetIndex; - if (segment != null) { - final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset); - if (rowIndex > segment.firstIndex) { - final rowIndexInSegment = rowIndex - (segment.firstIndex + 1); - final assetsPerRow = ref.read(timelineArgsProvider).columnCount; - final assetIndexInSegment = rowIndexInSegment * assetsPerRow; - targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment; - } else { - targetAssetIndex = segment.firstAssetIndex; - } - } - - setState(() { - _scaleFactor = newScaleFactor; - _perRow = newPerRow; - _scaleRestoreAssetIndex = targetAssetIndex; - }); - - ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); - } - }; - }, - ), + child: NotificationListener( + onNotification: (notification) { + final currentIndex = _getCurrentAssetIndex(segments); + if (currentIndex != null && mounted) { + _restoreAssetIndex = currentIndex; + } + return false; }, - child: TimelineDragRegion( - onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null, - onAssetEnter: _handleDragAssetEnter, - onEnd: !isReadonlyModeEnabled ? _stopDrag : null, - onScroll: _dragScroll, - onScrollStart: () { - // Minimize the bottom sheet when drag selection starts - ref.read(timelineStateProvider.notifier).setScrolling(true); + child: RawGestureDetector( + gestures: { + CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => CustomScaleGestureRecognizer(), + (CustomScaleGestureRecognizer scale) { + scale.onStart = (details) { + _baseScaleFactor = _scaleFactor; + }; + + scale.onUpdate = (details) { + final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0); + final newPerRow = 7 - newScaleFactor.toInt(); + final targetAssetIndex = _getCurrentAssetIndex(segments); + + if (newPerRow != _perRow) { + setState(() { + _scaleFactor = newScaleFactor; + _perRow = newPerRow; + _restoreAssetIndex = targetAssetIndex; + }); + + ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); + } + }; + }, + ), }, - child: Stack( - children: [ - timeline, - if (!isSelectionMode && isMultiSelectEnabled) ...[ - Positioned( - top: MediaQuery.paddingOf(context).top, - left: 25, - child: const SizedBox( - height: kToolbarHeight, - child: Center(child: _MultiSelectStatusButton()), + child: TimelineDragRegion( + onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null, + onAssetEnter: _handleDragAssetEnter, + onEnd: !isReadonlyModeEnabled ? _stopDrag : null, + onScroll: _dragScroll, + onScrollStart: () { + // Minimize the bottom sheet when drag selection starts + ref.read(timelineStateProvider.notifier).setScrolling(true); + }, + child: Stack( + children: [ + timeline, + if (!isSelectionMode && isMultiSelectEnabled) ...[ + Positioned( + top: MediaQuery.paddingOf(context).top, + left: 25, + child: const SizedBox( + height: kToolbarHeight, + child: Center(child: _MultiSelectStatusButton()), + ), ), - ), - if (widget.bottomSheet != null) widget.bottomSheet!, + if (widget.bottomSheet != null) widget.bottomSheet!, + ], ], - ], + ), ), ), ), diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index fba4fa7294..98300894f9 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -15,7 +15,6 @@ class ServerInfoNotifier extends StateNotifier { : super( const ServerInfo( serverVersion: ServerVersion(major: 0, minor: 0, patch: 0), - latestVersion: null, serverFeatures: ServerFeatures(map: true, trash: true, oauthEnabled: false, passwordLogin: true), serverConfig: ServerConfig( trashDays: 30, @@ -104,7 +103,9 @@ final serverInfoProvider = StateNotifierProvider final versionWarningPresentProvider = Provider.family((ref, user) { final serverInfo = ref.watch(serverInfoProvider); - return serverInfo.versionStatus == VersionStatus.clientOutOfDate || - serverInfo.versionStatus == VersionStatus.error || - ((user?.isAdmin ?? false) && serverInfo.versionStatus == VersionStatus.serverOutOfDate); + return switch (serverInfo.versionStatus) { + VersionStatus.clientOutOfDate || VersionStatus.error => true, + VersionStatus.serverOutOfDate => serverInfo.latestVersion != null && (user?.isAdmin ?? false), + VersionStatus.upToDate => false, + }; }); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 9468b105e5..2bc000db45 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -165,7 +165,7 @@ class AppRouter extends RootStackRouter { late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), AutoRoute(page: PermissionOnboardingRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]), + AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), AutoRoute( diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 0803cfcdf0..9d2bdbe4a0 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -4,6 +4,7 @@ import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/asset.service.dart' as beta_asset_service; import 'package:immich_mobile/domain/services/memory.service.dart'; +import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -13,6 +14,7 @@ import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -33,6 +35,7 @@ final deepLinkServiceProvider = Provider( ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), ref.watch(driftMemoryServiceProvider), + ref.watch(driftPeopleServiceProvider), ref.watch(currentUserProvider), ), ); @@ -49,7 +52,8 @@ class DeepLinkService { final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; - final DriftMemoryService _betaMemoryServiceProvider; + final DriftMemoryService _betaMemoryService; + final DriftPeopleService _betaPeopleService; final UserDto? _currentUser; @@ -62,7 +66,8 @@ class DeepLinkService { this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, - this._betaMemoryServiceProvider, + this._betaMemoryService, + this._betaPeopleService, this._currentUser, ); @@ -84,6 +89,7 @@ class DeepLinkService { "memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''), "asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref), "album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''), + "people" => await _buildPeopleDeepLink(queryParams['id'] ?? ''), "activity" => await _buildActivityDeepLink(queryParams['albumId'] ?? ''), _ => null, }; @@ -106,6 +112,7 @@ class DeepLinkService { const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; final assetRegex = RegExp('/photos/($uuidRegex)'); final albumRegex = RegExp('/albums/($uuidRegex)'); + final peopleRegex = RegExp('/people/($uuidRegex)'); PageRouteInfo? deepLinkRoute; if (assetRegex.hasMatch(path)) { @@ -114,6 +121,9 @@ class DeepLinkService { } else if (albumRegex.hasMatch(path)) { final albumId = albumRegex.firstMatch(path)?.group(1) ?? ''; deepLinkRoute = await _buildAlbumDeepLink(albumId); + } else if (peopleRegex.hasMatch(path)) { + final peopleId = peopleRegex.firstMatch(path)?.group(1) ?? ''; + deepLinkRoute = await _buildPeopleDeepLink(peopleId); } else if (path == "/memory") { deepLinkRoute = await _buildMemoryDeepLink(null); } @@ -136,9 +146,9 @@ class DeepLinkService { return null; } - memories = await _betaMemoryServiceProvider.getMemoryLane(_currentUser.id); + memories = await _betaMemoryService.getMemoryLane(_currentUser.id); } else { - final memory = await _betaMemoryServiceProvider.get(memoryId); + final memory = await _betaMemoryService.get(memoryId); if (memory != null) { memories = [memory]; } @@ -225,4 +235,18 @@ class DeepLinkService { return DriftActivitiesRoute(album: album); } + + Future _buildPeopleDeepLink(String personId) async { + if (Store.isBetaTimelineEnabled == false) { + return null; + } + + final person = await _betaPeopleService.get(personId); + + if (person == null) { + return null; + } + + return DriftPersonRoute(person: person); + } } diff --git a/mobile/lib/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index a633a04d7f..3837d6337c 100644 --- a/mobile/lib/theme/theme_data.dart +++ b/mobile/lib/theme/theme_data.dart @@ -73,7 +73,9 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale ), navigationBarTheme: NavigationBarThemeData( backgroundColor: isDark ? colorScheme.surfaceContainer : colorScheme.surface, - labelTextStyle: const WidgetStatePropertyAll(TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + labelTextStyle: const WidgetStatePropertyAll( + TextStyle(fontSize: 14, fontWeight: FontWeight.w500, overflow: TextOverflow.ellipsis), + ), ), inputDecorationTheme: InputDecorationTheme( focusedBorder: OutlineInputBorder( diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart index a61a284844..d21cdfbc94 100644 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ b/mobile/lib/widgets/activities/activity_text_field.dart @@ -63,7 +63,7 @@ class ActivityTextField extends HookConsumerWidget { prefixIcon: user != null ? Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30, radius: 15), + child: UserCircleAvatar(user: user, size: 30), ) : null, suffixIcon: Padding( diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart index e0eccbff21..ac3b6c95a4 100644 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ b/mobile/lib/widgets/activities/activity_tile.dart @@ -40,7 +40,7 @@ class ActivityTile extends HookConsumerWidget { child: Icon(Icons.thumb_up, color: context.primaryColor), ) : isBottomSheet - ? UserCircleAvatar(user: activity.user, size: 30, radius: 15) + ? UserCircleAvatar(user: activity.user, size: 30) : UserCircleAvatar(user: activity.user), title: _ActivityTitle( userName: activity.user.name, diff --git a/mobile/lib/widgets/activities/comment_bubble.dart b/mobile/lib/widgets/activities/comment_bubble.dart index 5f060833a7..401e4b8e99 100644 --- a/mobile/lib/widgets/activities/comment_bubble.dart +++ b/mobile/lib/widgets/activities/comment_bubble.dart @@ -41,7 +41,7 @@ class CommentBubble extends ConsumerWidget { // avatar (hidden for own messages) Widget avatar = const SizedBox.shrink(); if (!isOwn) { - avatar = UserCircleAvatar(user: activity.user, size: 28, radius: 14); + avatar = UserCircleAvatar(user: activity.user, size: 28); } // Thumbnail with tappable behavior and optional heart overlay diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart index 8913e94136..2025fa7583 100644 --- a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -33,7 +33,7 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget { itemBuilder: ((context, index) { return Padding( padding: const EdgeInsets.only(right: 4.0), - child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true), + child: UserCircleAvatar(user: sharedUsers[index], size: 36, hasBorder: true), ); }), itemCount: sharedUsers.length, diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index 58c73a77b8..527aae0e6e 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -153,42 +153,26 @@ class ImmichAppBarDialog extends HookConsumerWidget { percentage = user.quotaUsageInBytes / user.quotaSizeInBytes; } - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 4), - decoration: BoxDecoration(color: context.colorScheme.surface), - child: ListTile( - minLeadingWidth: 50, - leading: Icon(Icons.storage_rounded, color: theme.primaryColor), - title: Text( + return Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text( "backup_controller_page_server_storage", style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500), ).tr(), - isThreeLine: true, - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: LinearProgressIndicator( - minHeight: 10.0, - value: percentage, - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: const Text( - 'backup_controller_page_storage_format', - ).tr(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}), - ), - ], - ), + LinearProgressIndicator( + minHeight: 10.0, + value: percentage, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), - ), + Text( + 'backup_controller_page_storage_format', + style: context.textTheme.bodySmall, + ).tr(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}), + ], ), ); } @@ -275,9 +259,22 @@ class ImmichAppBarDialog extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ Container(padding: const EdgeInsets.symmetric(horizontal: 8), child: buildTopRow()), - const AppBarProfileInfoBox(), - buildStorageInformation(), - const AppBarServerInfo(), + Container( + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + margin: const EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: Column( + children: [ + const AppBarProfileInfoBox(), + const Divider(height: 3), + buildStorageInformation(), + const Divider(height: 3), + const AppBarServerInfo(), + ], + ), + ), if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), buildFreeUpSpaceButton(), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index bc1d608b10..12273849f2 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -34,7 +34,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); } - final userImage = UserCircleAvatar(radius: 22, size: 44, user: user); + final userImage = UserCircleAvatar(size: 44, user: user); if (uploadProfileImageStatus == UploadProfileStatus.loading) { return const SizedBox(height: 40, width: 40, child: ImmichLoadingIndicator(borderRadius: 20)); @@ -80,50 +80,40 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); } - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.colorScheme.surface, - borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)), - ), - child: ListTile( - minLeadingWidth: 50, - leading: GestureDetector( - onTap: pickUserProfileImage, - onLongPress: toggleReadonlyMode, - child: Stack( - clipBehavior: Clip.none, - children: [ - AbsorbPointer(child: buildUserProfileImage()), - if (!isReadonlyModeEnabled) - Positioned( - bottom: -5, - right: -8, - child: Material( - color: context.colorScheme.surfaceContainerHighest, - elevation: 3, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), - child: Padding( - padding: const EdgeInsets.all(5.0), - child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14), - ), - ), + return ListTile( + minLeadingWidth: 50, + leading: GestureDetector( + onTap: pickUserProfileImage, + onLongPress: toggleReadonlyMode, + child: Stack( + clipBehavior: Clip.none, + children: [ + AbsorbPointer(child: buildUserProfileImage()), + if (!isReadonlyModeEnabled) + Positioned( + bottom: -5, + right: -8, + child: Material( + color: context.colorScheme.surfaceContainerHighest, + elevation: 3, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14), ), - ], - ), - ), - title: Text( - authState.name, - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor, fontWeight: FontWeight.w500), - ), - subtitle: Text( - authState.userEmail, - style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), + ), + ), + ], ), ), + title: Text( + authState.name, + style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor, fontWeight: FontWeight.w500), + ), + subtitle: Text( + authState.userEmail, + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), ); } } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index a341d6395c..3203b18df7 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -23,8 +23,6 @@ class AppBarServerInfo extends HookConsumerWidget { final bool showVersionWarning = ref.watch(versionWarningPresentProvider(user)); final appInfo = useState({}); - const titleFontSize = 12.0; - const contentFontSize = 11.0; getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -37,189 +35,103 @@ class AppBarServerInfo extends HookConsumerWidget { return null; }, []); + const divider = Divider(thickness: 1); + return Padding( - padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), - child: Container( - decoration: BoxDecoration( - color: context.colorScheme.surface, - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (showVersionWarning) ...[ - const Padding(padding: EdgeInsets.symmetric(horizontal: 8.0), child: ServerUpdateNotification()), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - ], - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text( - "server_info_box_app_version".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text( - "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text( - "server_version".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text( - serverInfoState.serverVersion.major > 0 - ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}" - : "--", - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text( - "server_info_box_server_url".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - flex: 0, - child: Container( - width: 200, - padding: const EdgeInsets.only(right: 10.0), - child: Tooltip( - verticalOffset: 0, - decoration: BoxDecoration( - color: context.primaryColor.withValues(alpha: 0.9), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - textStyle: TextStyle( - color: context.isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - message: getServerUrl() ?? '--', - preferBelow: false, - triggerMode: TooltipTriggerMode.tap, - child: Text( - getServerUrl() ?? '--', - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), - textAlign: TextAlign.end, - ), - ), - ), - ), - ], - ), - if (serverInfoState.latestVersion != null) ...[ - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Row( - children: [ - if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) - const Padding( - padding: EdgeInsets.only(right: 5.0), - child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12), - ), - Text( - "latest_version".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text( - serverInfoState.latestVersion!.major > 0 - ? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}" - : "--", - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ], - ], + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (showVersionWarning) ...[const ServerUpdateNotification(), divider], + _ServerInfoItem( + label: "server_info_box_app_version".tr(), + text: "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", ), - ), + divider, + _ServerInfoItem( + label: "server_version".tr(), + text: serverInfoState.serverVersion.major > 0 + ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}" + : "--", + ), + divider, + _ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true), + if (serverInfoState.latestVersion != null) ...[ + divider, + _ServerInfoItem( + label: "latest_version".tr(), + text: serverInfoState.latestVersion!.major > 0 + ? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}" + : "--", + tooltip: true, + icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate + ? const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12) + : null, + ), + ], + ], ), ); } } + +class _ServerInfoItem extends StatelessWidget { + final String label; + final String text; + final bool tooltip; + final Icon? icon; + + static const titleFontSize = 12.0; + static const contentFontSize = 11.0; + + const _ServerInfoItem({required this.label, required this.text, this.tooltip = false, this.icon}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (icon != null) ...[icon as Widget, const SizedBox(width: 8)], + Text( + label, + style: TextStyle( + fontSize: titleFontSize, + color: context.textTheme.labelSmall?.color, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _maybeTooltip( + context, + Text( + text, + style: TextStyle( + fontSize: contentFontSize, + color: context.colorScheme.onSurfaceSecondary, + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + textAlign: TextAlign.end, + ), + ), + ), + ], + ); + } + + Widget _maybeTooltip(BuildContext context, Widget child) => tooltip + ? Tooltip( + verticalOffset: 0, + decoration: BoxDecoration( + color: context.primaryColor.withValues(alpha: 0.9), + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + textStyle: TextStyle(color: context.colorScheme.onPrimary, fontWeight: FontWeight.bold), + message: text, + preferBelow: false, + triggerMode: TooltipTriggerMode.tap, + child: child, + ) + : child; +} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index b3dc04236c..ebd8ed8b36 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -51,7 +51,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { ? const Icon(Icons.face_outlined, size: widgetSize) : Semantics( label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(radius: 17, size: 31, user: user), + child: UserCircleAvatar(size: 32, user: user), ), ), ); diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 95622c1e5a..939e9e27aa 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; @@ -46,45 +48,42 @@ class ImmichSliverAppBar extends ConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); - return SliverAnimatedOpacity( - duration: Durations.medium1, - opacity: isMultiSelectEnabled ? 0 : 1, - sliver: SliverAppBar( - backgroundColor: context.colorScheme.surface, - surfaceTintColor: context.colorScheme.surfaceTint, - elevation: 0, - scrolledUnderElevation: 1.0, - floating: floating, - pinned: pinned, - snap: snap, - expandedHeight: expandedHeight, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), - automaticallyImplyLeading: false, - centerTitle: false, - title: title ?? const _ImmichLogoWithText(), - actions: [ - if (isCasting && !isReadonlyModeEnabled) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, + return SliverIgnorePointer( + ignoring: isMultiSelectEnabled, + sliver: SliverAnimatedOpacity( + duration: Durations.medium1, + opacity: isMultiSelectEnabled ? 0 : 1, + sliver: SliverAppBar( + backgroundColor: context.colorScheme.surface, + surfaceTintColor: context.colorScheme.surfaceTint, + elevation: 0, + scrolledUnderElevation: 1.0, + floating: floating, + pinned: pinned, + snap: snap, + expandedHeight: expandedHeight, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), + automaticallyImplyLeading: false, + centerTitle: false, + title: title ?? const _ImmichLogoWithText(), + actions: [ + const _SyncStatusIndicator(), + if (isCasting && !isReadonlyModeEnabled) + IconButton( + onPressed: () => showDialog(context: context, builder: (context) => const CastDialog()), icon: Icon(isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded), ), - ), - const _SyncStatusIndicator(), - if (actions != null) - ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled) - IconButton( - icon: const Icon(Icons.palette_rounded), - onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()), - ), - if (showUploadButton && !isReadonlyModeEnabled) - const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()), - const Padding(padding: EdgeInsets.only(right: 20), child: _ProfileIndicator()), - ], + if (actions != null) ...actions!, + if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled) + IconButton( + onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()), + icon: const Icon(Icons.palette_rounded), + ), + if (showUploadButton && !isReadonlyModeEnabled) const _BackupIndicator(), + const _ProfileIndicator(), + const SizedBox(width: 8), + ], + ), ), ); } @@ -94,27 +93,14 @@ class _ImmichLogoWithText extends StatelessWidget { const _ImmichLogoWithText(); @override - Widget build(BuildContext context) { - return Builder( - builder: (BuildContext context) { - return Row( - children: [ - Builder( - builder: (context) { - return Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ); - }, - ), - ], - ); - }, - ); - } + Widget build(BuildContext context) => AnimatedOpacity( + opacity: IconTheme.of(context).opacity ?? 1, + duration: kThemeChangeDuration, + child: SvgPicture.asset( + context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', + height: 40, + ), + ); } class _ProfileIndicator extends ConsumerWidget { @@ -126,7 +112,7 @@ class _ProfileIndicator extends ConsumerWidget { final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user)); final serverInfoState = ref.watch(serverInfoProvider); - const widgetSize = 30.0; + const widgetSize = 32.0; // TODO: remove this when update Flutter version newer than 3.35.7 final isIpad = defaultTargetPlatform == TargetPlatform.iOS && !context.isMobile; @@ -146,27 +132,23 @@ class _ProfileIndicator extends ConsumerWidget { ); } - return InkWell( - onTap: () => showDialog( + return IconButton( + onPressed: () => showDialog( context: context, useRootNavigator: false, barrierDismissible: !isIpad, builder: (ctx) => const ImmichAppBarDialog(), ), onLongPress: () => toggleReadonlyMode(), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - decoration: BoxDecoration( - color: context.isDarkTheme ? Colors.black : Colors.white, - borderRadius: BorderRadius.circular(widgetSize / 2), - ), - child: Icon( + icon: Badge( + label: _BadgeLabel( + Icon( Icons.info, color: serverInfoState.versionStatus == VersionStatus.error ? context.colorScheme.error : context.primaryColor, size: widgetSize / 2, + semanticLabel: 'new_version_available'.tr(), ), ), backgroundColor: Colors.transparent, @@ -177,7 +159,16 @@ class _ProfileIndicator extends ConsumerWidget { ? const Icon(Icons.face_outlined, size: widgetSize) : Semantics( label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: AbsorbPointer(child: UserCircleAvatar(radius: 17, size: 31, user: user)), + child: AbsorbPointer( + child: Builder( + builder: (context) => UserCircleAvatar( + size: 32, + user: user, + opacity: IconTheme.of(context).opacity ?? 1, + hasBorder: true, + ), + ), + ), ), ), ); @@ -193,10 +184,9 @@ class _BackupIndicator extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final indicatorIcon = _getBackupBadgeIcon(context, ref); - return InkWell( - onTap: () => context.pushRoute(const DriftBackupRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( + return IconButton( + onPressed: () => context.pushRoute(const DriftBackupRoute()), + icon: Badge( label: indicatorIcon, backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, @@ -278,12 +268,14 @@ class _BadgeLabel extends StatelessWidget { @override Widget build(BuildContext context) { + final opacity = IconTheme.of(context).opacity ?? 1; + return Container( width: _kBadgeWidgetSize / 2, height: _kBadgeWidgetSize / 2, decoration: BoxDecoration( - color: backgroundColor ?? context.colorScheme.surfaceContainer, - border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)), + color: (backgroundColor ?? context.colorScheme.surfaceContainer).withValues(alpha: opacity), + border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3 * opacity)), borderRadius: BorderRadius.circular(_kBadgeWidgetSize / 2), ), child: indicator, @@ -346,23 +338,30 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with return const SizedBox.shrink(); } - return AnimatedBuilder( - animation: Listenable.merge([_rotationAnimation, _dismissalAnimation]), - builder: (context, child) { - return Padding( - padding: EdgeInsets.only(right: isSyncing ? 16 : 0), - child: Transform.scale( - scale: isSyncing ? 1.0 : _dismissalAnimation.value, - child: Opacity( - opacity: isSyncing ? 1.0 : _dismissalAnimation.value, - child: Transform.rotate( - angle: _rotationAnimation.value * 2 * 3.14159 * -1, // Rotate counter-clockwise - child: Icon(Icons.sync, size: 24, color: context.primaryColor), - ), - ), - ), - ); - }, + return Padding( + padding: const EdgeInsets.all(8), + child: TweenAnimationBuilder( + tween: Tween(end: IconTheme.of(context).opacity ?? 1), + duration: kThemeChangeDuration, + builder: (context, opacity, child) { + return AnimatedBuilder( + animation: Listenable.merge([_rotationAnimation, _dismissalAnimation]), + builder: (context, child) { + final dismissalValue = isSyncing ? 1.0 : _dismissalAnimation.value; + return IconTheme( + data: IconTheme.of(context).copyWith(opacity: opacity * dismissalValue), + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..scaleByDouble(dismissalValue, dismissalValue, dismissalValue, 1.0) + ..rotateZ(-_rotationAnimation.value * 2 * math.pi), + child: const Icon(Icons.sync), + ), + ); + }, + ); + }, + ), ); } } diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index 30eaf4c555..50746f5cbd 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'dart:ui'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -254,22 +254,9 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S ), GestureDetector( onTap: widget.onEditTitle, - child: SizedBox( - width: double.infinity, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - currentAlbum.name, - maxLines: 1, - style: const TextStyle( - color: Colors.white, - fontSize: 36, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black54)], - ), - ), - ), + child: LayoutBuilder( + builder: (context, constraints) => + _DynamicText(text: currentAlbum.name, maxWidth: constraints.maxWidth), ), ), if (currentAlbum.description.isNotEmpty) @@ -549,3 +536,46 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic ); } } + +class _DynamicText extends StatelessWidget { + final String text; + final double maxWidth; + + const _DynamicText({required this.text, required this.maxWidth}); + + static const _baseTextStyle = TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black54)], + overflow: TextOverflow.ellipsis, + ); + + int _lineCount(double fontSize) { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: _baseTextStyle.copyWith(fontSize: fontSize), + ), + maxLines: 3, + textDirection: TextDirection.ltr, + )..layout(maxWidth: maxWidth); + return textPainter.computeLineMetrics().length; + } + + double _fontSize() { + final fontSizes = [44.0, 36.0]; + for (final fontSize in fontSizes) { + final lineCount = _lineCount(fontSize); + if (lineCount == 1) { + return fontSize; + } + } + return 28; + } + + @override + Widget build(BuildContext context) { + return Text(text, style: _baseTextStyle.copyWith(fontSize: _fontSize()), maxLines: 3); + } +} diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index 352d686e7c..fe39c5da3f 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -8,49 +8,52 @@ import 'package:immich_mobile/presentation/widgets/images/remote_image_provider. // ignore: must_be_immutable class UserCircleAvatar extends ConsumerWidget { final UserDto user; - double radius; double size; bool hasBorder; + double opacity; - UserCircleAvatar({super.key, this.radius = 22, this.size = 44, this.hasBorder = false, required this.user}); + UserCircleAvatar({super.key, this.size = 44, this.hasBorder = false, this.opacity = 1, required this.user}); @override Widget build(BuildContext context, WidgetRef ref) { - final userAvatarColor = user.avatarColor.toColor(); + final userAvatarColor = user.avatarColor.toColor().withValues(alpha: opacity); final profileImageUrl = '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}'; + final textColor = (user.avatarColor.toColor().computeLuminance() > 0.5 ? Colors.black : Colors.white).withValues( + alpha: opacity, + ); + final textIcon = DefaultTextStyle( - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: userAvatarColor.computeLuminance() > 0.5 ? Colors.black : Colors.white, - ), + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: textColor), child: Text(user.name[0].toUpperCase()), ); return Tooltip( message: user.name, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: hasBorder ? Border.all(color: Colors.grey[500]!, width: 1) : null, - ), - child: CircleAvatar( - backgroundColor: userAvatarColor, - radius: radius, + child: UnconstrainedBox( + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: userAvatarColor, + shape: BoxShape.circle, + border: hasBorder ? Border.all(color: Colors.grey[500]!.withValues(alpha: opacity), width: 1) : null, + ), child: user.hasProfileImage ? ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(50)), + borderRadius: BorderRadius.all(Radius.circular(size / 2)), child: Image( fit: BoxFit.cover, width: size, height: size, image: RemoteImageProvider(url: profileImageUrl), errorBuilder: (context, error, stackTrace) => textIcon, + color: Colors.white.withValues(alpha: opacity), + colorBlendMode: BlendMode.modulate, ), ) - : textIcon, + : Center(child: textIcon), ), ), ); diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 5d82630fc6..2d5c9f06eb 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -24,11 +25,12 @@ class LayoutSettings extends HookConsumerWidget { title: "asset_list_layout_sub_title".t(context: context), icon: Icons.view_module_outlined, ), - SettingsSwitchListTile( - valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), - ), + if (!Store.isBetaTimelineEnabled) + SettingsSwitchListTile( + valueNotifier: useDynamicLayout, + title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), SettingsSliderListTile( valueNotifier: tilesPerRow, text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), diff --git a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart index c3bb64faf6..d7e547054e 100644 --- a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart +++ b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart @@ -1,9 +1,8 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; class CustomProxyHeaderSettings extends StatelessWidget { @@ -15,11 +14,11 @@ class CustomProxyHeaderSettings extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 20), dense: true, title: Text( - IntlKeys.advanced_settings_proxy_headers_title.tr(), + context.t.advanced_settings_proxy_headers_title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), ), subtitle: Text( - IntlKeys.advanced_settings_proxy_headers_subtitle.tr(), + context.t.advanced_settings_proxy_headers_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), onTap: () => context.pushRoute(const HeaderSettingsRoute()), diff --git a/mobile/makefile b/mobile/makefile index 5f0a1a9f05..3a0a263687 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -41,7 +41,7 @@ translation: dart run easy_localization:generate -S ../i18n dart run bin/generate_keys.dart dart format lib/generated/codegen_loader.g.dart - dart format lib/generated/intl_keys.g.dart + dart format lib/generated/translations.g.dart analyze: dart analyze --fatal-infos diff --git a/mobile/mise.toml b/mobile/mise.toml index cdafd1cc18..6767836aa3 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -32,13 +32,7 @@ depends = [ [tasks."codegen:translation"] alias = "translation" description = "Generate translations from i18n JSONs" -run = [ - { task = "//i18n:format-fix" }, - { tasks = [ - "i18n:loader", - "i18n:keys", - ] }, -] +run = [{ task = "//i18n:format-fix" }, { tasks = ["i18n:loader", "i18n:keys"] }] [tasks."codegen:app-icon"] description = "Generate app icons" @@ -158,10 +152,10 @@ run = [ description = "Generate i18n keys" hide = true sources = ["i18n/en.json"] -outputs = "lib/generated/intl_keys.g.dart" +outputs = "lib/generated/translations.g.dart" run = [ "dart run bin/generate_keys.dart", - "dart format lib/generated/intl_keys.g.dart", + "dart format lib/generated/translations.g.dart", ] [tasks."analyze:dart"] diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 80c1a1c868..4ebe5c7c65 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 2.5.5 +- API version: 2.5.6 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -256,6 +256,7 @@ Class | Method | HTTP request | Description *SharedLinksApi* | [**getSharedLinkById**](doc//SharedLinksApi.md#getsharedlinkbyid) | **GET** /shared-links/{id} | Retrieve a shared link *SharedLinksApi* | [**removeSharedLink**](doc//SharedLinksApi.md#removesharedlink) | **DELETE** /shared-links/{id} | Delete a shared link *SharedLinksApi* | [**removeSharedLinkAssets**](doc//SharedLinksApi.md#removesharedlinkassets) | **DELETE** /shared-links/{id}/assets | Remove assets from a shared link +*SharedLinksApi* | [**sharedLinkLogin**](doc//SharedLinksApi.md#sharedlinklogin) | **POST** /shared-links/login | Shared link login *SharedLinksApi* | [**updateSharedLink**](doc//SharedLinksApi.md#updatesharedlink) | **PATCH** /shared-links/{id} | Update a shared link *StacksApi* | [**createStack**](doc//StacksApi.md#createstack) | **POST** /stacks | Create a stack *StacksApi* | [**deleteStack**](doc//StacksApi.md#deletestack) | **DELETE** /stacks/{id} | Delete a stack @@ -552,6 +553,7 @@ Class | Method | HTTP request | Description - [SetMaintenanceModeDto](doc//SetMaintenanceModeDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) + - [SharedLinkLoginDto](doc//SharedLinkLoginDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) - [SharedLinkType](doc//SharedLinkType.md) - [SharedLinksResponse](doc//SharedLinksResponse.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 90e426b547..f10490e093 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -292,6 +292,7 @@ part 'model/session_update_dto.dart'; part 'model/set_maintenance_mode_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; +part 'model/shared_link_login_dto.dart'; part 'model/shared_link_response_dto.dart'; part 'model/shared_link_type.dart'; part 'model/shared_links_response.dart'; diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 7f11db76d3..37eeffcf46 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -495,6 +495,77 @@ class SharedLinksApi { return null; } + /// Shared link login + /// + /// Login to a password protected shared link + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [SharedLinkLoginDto] sharedLinkLoginDto (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future sharedLinkLoginWithHttpInfo(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/shared-links/login'; + + // ignore: prefer_final_locals + Object? postBody = sharedLinkLoginDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Shared link login + /// + /// Login to a password protected shared link + /// + /// Parameters: + /// + /// * [SharedLinkLoginDto] sharedLinkLoginDto (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future sharedLinkLogin(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { + final response = await sharedLinkLoginWithHttpInfo(sharedLinkLoginDto, key: key, slug: slug, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto; + + } + return null; + } + /// Update a shared link /// /// Update an existing shared link by its ID. diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 7f5cd50ed4..470f3aec27 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -630,6 +630,8 @@ class ApiClient { return SharedLinkCreateDto.fromJson(value); case 'SharedLinkEditDto': return SharedLinkEditDto.fromJson(value); + case 'SharedLinkLoginDto': + return SharedLinkLoginDto.fromJson(value); case 'SharedLinkResponseDto': return SharedLinkResponseDto.fromJson(value); case 'SharedLinkType': diff --git a/mobile/openapi/lib/model/shared_link_login_dto.dart b/mobile/openapi/lib/model/shared_link_login_dto.dart new file mode 100644 index 0000000000..1ab1bc9349 --- /dev/null +++ b/mobile/openapi/lib/model/shared_link_login_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SharedLinkLoginDto { + /// Returns a new [SharedLinkLoginDto] instance. + SharedLinkLoginDto({ + required this.password, + }); + + /// Shared link password + String password; + + @override + bool operator ==(Object other) => identical(this, other) || other is SharedLinkLoginDto && + other.password == password; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (password.hashCode); + + @override + String toString() => 'SharedLinkLoginDto[password=$password]'; + + Map toJson() { + final json = {}; + json[r'password'] = this.password; + return json; + } + + /// Returns a new [SharedLinkLoginDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SharedLinkLoginDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkLoginDto"); + if (value is Map) { + final json = value.cast(); + + return SharedLinkLoginDto( + password: mapValueOfType(json, r'password')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SharedLinkLoginDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SharedLinkLoginDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SharedLinkLoginDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SharedLinkLoginDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'password', + }; +} + diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 077544b4f7..28adfc2ab7 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1217,10 +1217,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1910,10 +1910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" thumbhash: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 19b41f2799..0b54dfc53e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 2.5.5+3036 +version: 2.5.6+3037 environment: sdk: '>=3.8.0 <4.0.0' diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 1fe0fff6ae..d9f18b3007 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -21,6 +21,7 @@ import 'schema_v15.dart' as v15; import 'schema_v16.dart' as v16; import 'schema_v17.dart' as v17; import 'schema_v18.dart' as v18; +import 'schema_v19.dart' as v19; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -62,6 +63,8 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v17.DatabaseAtV17(db); case 18: return v18.DatabaseAtV18(db); + case 19: + return v19.DatabaseAtV19(db); default: throw MissingSchemaException(version, versions); } @@ -86,5 +89,6 @@ class GeneratedHelper implements SchemaInstantiationHelper { 16, 17, 18, + 19, ]; } diff --git a/mobile/test/drift/main/generated/schema_v19.dart b/mobile/test/drift/main/generated/schema_v19.dart new file mode 100644 index 0000000000..4a8dea806e --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v19.dart @@ -0,0 +1,8397 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + LocalAssetEntity createAlias(String alias) { + return LocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + final int orientation; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const LocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite, + required this.orientation, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory LocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + LocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + LocalAssetEntityData copyWithCompanion(LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class LocalAssetEntityCompanion extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const LocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}orientation'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV19 extends GeneratedDatabase { + DatabaseAtV19(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAlbumAssetAlbumAsset = Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAlbumOwnerId = Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxStackPrimaryAssetId = Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Index idxRemoteAssetStackId = Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + late final Index idxRemoteAssetLocalDateTimeDay = Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + late final Index idxRemoteAssetLocalDateTimeMonth = Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxPartnerSharedWithId = Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteAlbumAssetAlbumAsset = Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxPersonOwnerId = Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + late final Index idxAssetFacePersonId = Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + late final Index idxAssetFaceAssetId = Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 19; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart b/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart new file mode 100644 index 0000000000..a25a9d92a7 --- /dev/null +++ b/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart @@ -0,0 +1,51 @@ +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +void main() { + late Drift db; + + setUp(() { + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + }); + + tearDown(() async { + await db.close(); + }); + + test('mergedBucket falls back to createdAt when localDateTime is null', () async { + const userId = 'user-1'; + final createdAt = DateTime(2024, 1, 1, 12); + + await db + .into(db.userEntity) + .insert(UserEntityCompanion.insert(id: userId, email: 'user-1@test.dev', name: 'User 1')); + + await db + .into(db.remoteAssetEntity) + .insert( + RemoteAssetEntityCompanion.insert( + id: 'asset-1', + name: 'asset-1.jpg', + type: AssetType.image, + checksum: 'checksum-1', + ownerId: userId, + visibility: AssetVisibility.timeline, + createdAt: Value(createdAt), + updatedAt: Value(createdAt), + localDateTime: const Value(null), + ), + ); + + final buckets = await db.mergedAssetDrift.mergedBucket(groupBy: GroupAssetsBy.day.index, userIds: [userId]).get(); + + expect(buckets, hasLength(1)); + expect(buckets.single.assetCount, 1); + expect(buckets.single.bucketDate, isNotEmpty); + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 901d85c02c..13d6ba7e56 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11239,6 +11239,78 @@ "x-immich-state": "Stable" } }, + "/shared-links/login": { + "post": { + "description": "Login to a password protected shared link", + "operationId": "sharedLinkLogin", + "parameters": [ + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkLoginDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Shared link login", + "tags": [ + "Shared links" + ], + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Beta" + } + ], + "x-immich-state": "Beta" + } + }, "/shared-links/me": { "get": { "description": "Retrieve the current shared link associated with authentication method.", @@ -15072,7 +15144,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "2.5.5", + "version": "2.5.6", "contact": {} }, "tags": [ @@ -21686,6 +21758,19 @@ }, "type": "object" }, + "SharedLinkLoginDto": { + "properties": { + "password": { + "description": "Shared link password", + "example": "password", + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, "SharedLinkResponseDto": { "properties": { "album": { @@ -21744,9 +21829,25 @@ "type": "string" }, "token": { + "deprecated": true, "description": "Access token", "nullable": true, - "type": "string" + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Deprecated" + } + ], + "x-immich-state": "Deprecated" }, "type": { "allOf": [ diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 96f858e6d8..6310316857 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "2.5.5", + "version": "2.5.6", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^24.10.9", + "@types/node": "^24.10.11", "typescript": "^5.3.3" }, "repository": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index e6c31aeeae..59a25d58b3 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 2.5.5 + * 2.5.6 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -2287,6 +2287,10 @@ export type SharedLinkCreateDto = { /** Shared link type */ "type": SharedLinkType; }; +export type SharedLinkLoginDto = { + /** Shared link password */ + password: string; +}; export type SharedLinkEditDto = { /** Allow downloads */ allowDownload?: boolean; @@ -5861,6 +5865,26 @@ export function createSharedLink({ sharedLinkCreateDto }: { body: sharedLinkCreateDto }))); } +/** + * Shared link login + */ +export function sharedLinkLogin({ key, slug, sharedLinkLoginDto }: { + key?: string; + slug?: string; + sharedLinkLoginDto: SharedLinkLoginDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: SharedLinkResponseDto; + }>(`/shared-links/login${QS.query(QS.explode({ + key, + slug + }))}`, oazapfts.json({ + ...opts, + method: "POST", + body: sharedLinkLoginDto + }))); +} /** * Retrieve current shared link */ diff --git a/package.json b/package.json index 3e04703974..0e4017f928 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "immich-monorepo", - "version": "2.5.5", + "version": "2.5.6", "description": "Monorepo for Immich", "private": true, - "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48", + "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264", "engines": { "pnpm": ">=10.0.0" } diff --git a/plugins/package-lock.json b/plugins/package-lock.json index 1d8b9cb1ad..9ebaa59a02 100644 --- a/plugins/package-lock.json +++ b/plugins/package-lock.json @@ -15,9 +15,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -32,9 +32,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -49,9 +49,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -66,9 +66,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -83,9 +83,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -100,9 +100,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -117,9 +117,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -134,9 +134,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -151,9 +151,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -168,9 +168,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -185,9 +185,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -202,9 +202,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -219,9 +219,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -236,9 +236,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -253,9 +253,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -270,9 +270,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -287,9 +287,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -321,9 +321,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -338,9 +338,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -355,9 +355,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -372,9 +372,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -389,9 +389,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -406,9 +406,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -423,9 +423,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -440,9 +440,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -467,9 +467,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -480,32 +480,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/typescript": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84b83690e4..7366840245 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: devDependencies: prettier: specifier: ^3.7.4 - version: 3.8.0 + version: 3.8.1 cli: dependencies: @@ -45,7 +45,7 @@ importers: specifier: ^9.8.0 version: 9.39.2 '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@types/byte-size': specifier: ^8.1.0 @@ -63,11 +63,11 @@ importers: specifier: ^4.13.1 version: 4.13.4 '@types/node': - specifier: ^24.10.9 - version: 24.10.9 + specifier: ^24.10.11 + version: 24.10.13 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -85,7 +85,7 @@ importers: version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -97,28 +97,28 @@ importers: version: 5.5.0 prettier: specifier: ^3.7.4 - version: 3.8.0 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.0.0 - version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.1.0(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest-fetch-mock: specifier: ^0.4.0 - version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) yaml: specifier: ^2.3.1 version: 2.8.2 @@ -127,16 +127,16 @@ importers: dependencies: '@docusaurus/core': specifier: ~3.9.0 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/preset-classic': specifier: ~3.9.0 - version: 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + version: 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/theme-common': specifier: ~3.9.0 - version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-mermaid': specifier: ~3.9.0 - version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -145,13 +145,13 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.8)(react@18.3.1) + version: 3.1.1(@types/react@19.2.13)(react@18.3.1) autoprefixer: specifier: ^10.4.17 - version: 10.4.23(postcss@8.5.6) + version: 10.4.24(postcss@8.5.6) docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lunr: specifier: ^2.3.9 version: 2.3.9 @@ -188,7 +188,7 @@ importers: version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) prettier: specifier: ^3.7.4 - version: 3.8.0 + version: 3.8.1 typescript: specifier: ^5.1.6 version: 5.9.3 @@ -200,19 +200,19 @@ importers: version: 9.39.2 '@faker-js/faker': specifier: ^10.1.0 - version: 10.2.0 + version: 10.3.0 '@immich/cli': - specifier: file:../cli + specifier: workspace:* version: link:../cli '@immich/e2e-auth-server': - specifier: file:../e2e-auth-server + specifier: workspace:* version: link:../e2e-auth-server '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@playwright/test': specifier: ^1.44.1 - version: 1.57.0 + version: 1.58.2 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 @@ -220,8 +220,8 @@ importers: specifier: ^3.4.2 version: 3.7.1 '@types/node': - specifier: ^24.10.9 - version: 24.10.9 + specifier: ^24.10.11 + version: 24.10.13 '@types/pg': specifier: ^8.15.1 version: 8.16.0 @@ -233,7 +233,7 @@ importers: version: 6.0.3 dotenv: specifier: ^17.2.3 - version: 17.2.3 + version: 17.2.4 eslint: specifier: ^9.14.0 version: 9.39.2(jiti@2.6.1) @@ -242,7 +242,7 @@ importers: version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -257,16 +257,16 @@ importers: version: 3.7.2 pg: specifier: ^8.11.3 - version: 8.17.1 + version: 8.18.0 pngjs: specifier: ^7.0.0 version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.8.0 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -281,13 +281,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) utimes: specifier: ^5.2.1 version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) e2e-auth-server: devDependencies: @@ -308,10 +308,10 @@ importers: devDependencies: prettier: specifier: ^3.7.4 - version: 3.8.0 + version: 3.8.1 prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.2.0(prettier@3.8.0) + version: 4.2.0(prettier@3.8.1) open-api/typescript-sdk: dependencies: @@ -320,8 +320,8 @@ importers: version: 1.1.0 devDependencies: '@types/node': - specifier: ^24.10.9 - version: 24.10.9 + specifier: ^24.10.11 + version: 24.10.13 typescript: specifier: ^5.3.3 version: 5.9.3 @@ -333,7 +333,7 @@ importers: version: 1.1.1 esbuild: specifier: ^0.27.0 - version: 0.27.2 + version: 0.27.3 typescript: specifier: ^5.3.2 version: 5.9.3 @@ -345,67 +345,67 @@ importers: version: 2.0.0-rc13 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(bullmq@5.66.5) + version: 11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(bullmq@5.67.3) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.4 - version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2) + version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.13)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + version: 6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) '@nestjs/swagger': specifier: ^11.0.2 - version: 11.2.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + version: 11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 '@opentelemetry/context-async-hooks': specifier: ^2.0.0 - version: 2.4.0(@opentelemetry/api@1.9.0) + version: 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': - specifier: ^0.210.0 - version: 0.210.0(@opentelemetry/api@1.9.0) + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: ^0.210.0 - version: 0.210.0(@opentelemetry/api@1.9.0) + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-ioredis': - specifier: ^0.58.0 - version: 0.58.0(@opentelemetry/api@1.9.0) + specifier: ^0.59.0 + version: 0.59.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.56.0 - version: 0.56.0(@opentelemetry/api@1.9.0) + specifier: ^0.57.0 + version: 0.57.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-pg': - specifier: ^0.62.0 - version: 0.62.0(@opentelemetry/api@1.9.0) + specifier: ^0.63.0 + version: 0.63.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.4.0(@opentelemetry/api@1.9.0) + version: 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.4.0(@opentelemetry/api@1.9.0) + version: 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: ^0.210.0 - version: 0.210.0(@opentelemetry/api@1.9.0) + specifier: ^0.211.0 + version: 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 - version: 1.38.0 + version: 1.39.0 '@react-email/components': specifier: ^0.5.0 - version: 0.5.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-email/render': specifier: ^1.1.2 - version: 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.6) @@ -426,7 +426,7 @@ importers: version: 2.2.2 bullmq: specifier: ^5.51.0 - version: 5.66.5 + version: 5.67.3 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -446,8 +446,8 @@ importers: specifier: ^1.4.7 version: 1.4.7 cron: - specifier: 4.3.5 - version: 4.3.5 + specifier: 4.4.0 + version: 4.4.0 exiftool-vendored: specifier: ^34.3.0 version: 34.3.0 @@ -471,7 +471,7 @@ importers: version: 7.14.0 ioredis: specifier: ^5.8.2 - version: 5.9.1 + version: 5.9.2 jose: specifier: ^5.10.0 version: 5.10.0 @@ -501,28 +501,28 @@ importers: version: 2.0.2 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@types/inquirer@8.2.12)(@types/node@24.10.9)(typescript@5.9.3) + version: 3.20.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@types/inquirer@8.2.12)(@types/node@24.10.13)(typescript@5.9.3) nestjs-cls: specifier: ^5.0.0 - version: 5.4.3(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 5.4.3(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(kysely@0.28.2)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(kysely@0.28.2)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^7.0.0 - version: 7.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + version: 7.0.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) nodemailer: specifier: ^7.0.0 - version: 7.0.12 + version: 7.0.13 openid-client: specifier: ^6.3.3 version: 6.8.1 pg: specifier: ^8.11.3 - version: 8.17.1 + version: 8.18.0 pg-connection-string: specifier: ^2.9.1 - version: 2.10.0 + version: 2.11.0 picomatch: specifier: ^4.0.2 version: 4.0.3 @@ -531,10 +531,10 @@ importers: version: 3.4.8 react: specifier: ^19.0.0 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.0.0 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) react-email: specifier: ^4.0.0 version: 4.3.2 @@ -552,7 +552,7 @@ importers: version: 2.17.0 semver: specifier: ^7.6.2 - version: 7.7.3 + version: 7.7.4 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -573,7 +573,7 @@ importers: version: 3.1.0 ua-parser-js: specifier: ^2.0.0 - version: 2.0.8 + version: 2.0.9 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -586,16 +586,16 @@ importers: version: 9.39.2 '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.15(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9) + version: 11.0.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@24.10.13) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.4 - version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12) + version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13) '@swc/core': specifier: ^1.4.14 - version: 1.15.8(@swc/helpers@0.5.17) + version: 1.15.11(@swc/helpers@0.5.17) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -639,11 +639,11 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@types/node': - specifier: ^24.10.9 - version: 24.10.9 + specifier: ^24.10.11 + version: 24.10.13 '@types/nodemailer': specifier: ^7.0.0 - version: 7.0.5 + version: 7.0.9 '@types/picomatch': specifier: ^4.0.0 version: 4.0.2 @@ -652,7 +652,7 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.2.8 + version: 19.2.13 '@types/sanitize-html': specifier: ^2.13.0 version: 2.16.0 @@ -670,7 +670,7 @@ importers: version: 13.15.10 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) eslint: specifier: ^9.14.0 version: 9.39.2(jiti@2.6.1) @@ -679,7 +679,7 @@ importers: version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -691,16 +691,16 @@ importers: version: 5.5.0 node-gyp: specifier: ^12.0.0 - version: 12.1.0 + version: 12.2.0 pngjs: specifier: ^7.0.0 version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.8.0 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) sql-formatter: specifier: ^15.0.0 version: 15.7.0 @@ -718,31 +718,31 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.55.1) + version: 1.5.9(@swc/core@1.15.11(@swc/helpers@0.5.17))(rollup@4.55.1) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.1.0(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web: dependencies: '@formatjs/icu-messageformat-parser': specifier: ^3.0.0 - version: 3.3.0 + version: 3.5.1 '@immich/justified-layout-wasm': specifier: ^0.4.3 version: 0.4.3 '@immich/sdk': - specifier: file:../open-api/typescript-sdk - version: file:open-api/typescript-sdk + specifier: workspace:* + version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.61.4 - version: 0.61.4(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) + specifier: ^0.62.1 + version: 0.62.1(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -771,17 +771,17 @@ importers: specifier: ^7946.0.16 version: 7946.0.16 '@zoom-image/core': - specifier: ^0.41.0 - version: 0.41.4 + specifier: ^0.42.0 + version: 0.42.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.8(svelte@5.48.0) + version: 0.3.9(svelte@5.50.0) dom-to-image: specifier: ^2.6.0 version: 2.6.0 fabric: specifier: ^6.5.4 - version: 6.9.1(encoding@0.1.13) + version: 6.9.1 geo-coordinates-parser: specifier: ^1.7.4 version: 1.7.4 @@ -793,10 +793,10 @@ importers: version: 4.7.8 happy-dom: specifier: ^20.0.0 - version: 20.3.0 + version: 20.5.0 intl-messageformat: specifier: ^11.0.0 - version: 11.0.9 + version: 11.1.2 justified-layout: specifier: ^4.1.0 version: 4.1.0 @@ -808,13 +808,13 @@ importers: version: 3.7.2 maplibre-gl: specifier: ^5.6.2 - version: 5.16.0 + version: 5.17.0 media-chrome: specifier: ^4.17.2 - version: 4.17.2(react@19.2.3) + version: 4.17.2(react@19.2.4) pmtiles: specifier: ^4.3.0 - version: 4.3.2 + version: 4.4.0 qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -829,16 +829,16 @@ importers: version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.48.0) + version: 4.0.1(svelte@5.50.0) svelte-jsoneditor: specifier: ^3.10.0 - version: 3.11.0(svelte@5.48.0) + version: 3.11.0(svelte@5.50.0) svelte-maplibre: specifier: ^1.2.5 - version: 1.2.5(svelte@5.48.0) + version: 1.2.6(svelte@5.50.0) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.48.0) + version: 0.12.0(svelte@5.50.0) tabbable: specifier: ^6.2.0 version: 6.4.0 @@ -857,7 +857,7 @@ importers: version: 9.39.2 '@faker-js/faker': specifier: ^10.0.0 - version: 10.2.0 + version: 10.3.0 '@koddsson/eslint-plugin-tscompat': specifier: ^0.2.0 version: 0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) @@ -866,25 +866,25 @@ importers: version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) + version: 3.0.10(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) '@sveltejs/enhanced-img': - specifier: ^0.9.0 - version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^0.10.0 + version: 0.10.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': specifier: 6.2.4 - version: 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.1.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.3.1(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) @@ -908,10 +908,10 @@ importers: version: 1.5.6 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) dotenv: specifier: ^17.0.0 - version: 17.2.3 + version: 17.2.4 eslint: specifier: ^9.36.0 version: 9.39.2(jiti@2.6.1) @@ -920,10 +920,10 @@ importers: version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-compat: specifier: ^6.0.2 - version: 6.0.2(eslint@9.39.2(jiti@2.6.1)) + version: 6.1.0(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-svelte: specifier: ^3.12.4 - version: 3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.48.0) + version: 3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.50.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -935,28 +935,28 @@ importers: version: 16.5.0 prettier: specifier: ^3.7.4 - version: 3.8.0 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.8.0)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.2.0(prettier@3.8.0) + version: 4.2.0(prettier@3.8.1) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.1(prettier@3.8.0)(svelte@5.48.0) + version: 3.4.1(prettier@3.8.1)(svelte@5.50.0) rollup-plugin-visualizer: specifier: ^6.0.0 version: 6.0.5(rollup@4.55.1) svelte: - specifier: 5.48.0 - version: 5.48.0 + specifier: 5.50.0 + version: 5.50.0 svelte-check: specifier: ^4.1.5 - version: 4.3.5(picomatch@4.0.3)(svelte@5.48.0)(typescript@5.9.3) + version: 4.3.6(picomatch@4.0.3)(svelte@5.50.0)(typescript@5.9.3) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.4.1(svelte@5.48.0) + version: 1.4.1(svelte@5.50.0) tailwindcss: specifier: ^4.1.7 version: 4.1.18 @@ -965,13 +965,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.45.0 - version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -1122,137 +1122,8 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-sesv2@3.971.0': - resolution: {integrity: sha512-NP/lbf3mfY10Txzl0ml2YnTjnZwflp1+faOotMCrXi4fb6kInosdW0ZSHXNlNulFo9cW+llq07lD59Sw3nny+A==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/client-sso@3.971.0': - resolution: {integrity: sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/core@3.970.0': - resolution: {integrity: sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-env@3.970.0': - resolution: {integrity: sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.970.0': - resolution: {integrity: sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.971.0': - resolution: {integrity: sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-login@3.971.0': - resolution: {integrity: sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-node@3.971.0': - resolution: {integrity: sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-process@3.970.0': - resolution: {integrity: sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-sso@3.971.0': - resolution: {integrity: sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.971.0': - resolution: {integrity: sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-host-header@3.969.0': - resolution: {integrity: sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-logger@3.969.0': - resolution: {integrity: sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-recursion-detection@3.969.0': - resolution: {integrity: sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-sdk-s3@3.970.0': - resolution: {integrity: sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-user-agent@3.970.0': - resolution: {integrity: sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/nested-clients@3.971.0': - resolution: {integrity: sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/region-config-resolver@3.969.0': - resolution: {integrity: sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.970.0': - resolution: {integrity: sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.971.0': - resolution: {integrity: sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.969.0': - resolution: {integrity: sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-arn-parser@3.968.0': - resolution: {integrity: sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-endpoints@3.970.0': - resolution: {integrity: sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-locate-window@3.965.2': - resolution: {integrity: sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-user-agent-browser@3.969.0': - resolution: {integrity: sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==} - - '@aws-sdk/util-user-agent-node@3.971.0': - resolution: {integrity: sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.969.0': - resolution: {integrity: sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.2.3': - resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} - engines: {node: '>=18.0.0'} - - '@babel/code-frame@7.28.6': - resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.28.5': @@ -2399,8 +2270,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -2417,8 +2288,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -2435,8 +2306,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -2453,8 +2324,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -2471,8 +2342,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -2489,8 +2360,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -2507,8 +2378,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -2525,8 +2396,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -2543,8 +2414,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -2561,8 +2432,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -2579,8 +2450,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -2597,8 +2468,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -2615,8 +2486,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -2633,8 +2504,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -2651,8 +2522,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -2669,8 +2540,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -2687,8 +2558,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -2699,8 +2570,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -2717,8 +2588,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -2729,8 +2600,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -2747,8 +2618,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -2759,8 +2630,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -2777,8 +2648,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -2795,8 +2666,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -2813,8 +2684,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -2831,8 +2702,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2881,8 +2752,8 @@ packages: '@extism/js-pdk@1.1.1': resolution: {integrity: sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==} - '@faker-js/faker@10.2.0': - resolution: {integrity: sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==} + '@faker-js/faker@10.3.0': + resolution: {integrity: sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} '@fig/complete-commander@3.2.0': @@ -2902,32 +2773,32 @@ packages: '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} - '@formatjs/ecma402-abstract@3.0.8': - resolution: {integrity: sha512-NRiqvxAvhbARZRFSRFPjN0y8txxmVutv2vMYvW2HSdCVf58w9l4osLj6Ujif643vImwZBcbKqhiKE0IOhY+DvA==} + '@formatjs/ecma402-abstract@3.1.1': + resolution: {integrity: sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==} '@formatjs/fast-memoize@2.2.7': resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} - '@formatjs/fast-memoize@3.0.3': - resolution: {integrity: sha512-CArYtQKGLAOruCMeq5/RxCg6vUXFx3OuKBdTm30Wn/+gCefehmZ8Y2xSMxMrO2iel7hRyE3HKfV56t3vAU6D4Q==} + '@formatjs/fast-memoize@3.1.0': + resolution: {integrity: sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==} '@formatjs/icu-messageformat-parser@2.11.4': resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} - '@formatjs/icu-messageformat-parser@3.3.0': - resolution: {integrity: sha512-dqxGSwH22ZfBwa6EVvrrIo+8kHHUSjuw9iZy6HkkN5XgH5/8ny9zDGhvC6ZOFYp01PAbwHvUTIHqznC6Z1nIbA==} + '@formatjs/icu-messageformat-parser@3.5.1': + resolution: {integrity: sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==} '@formatjs/icu-skeleton-parser@1.8.16': resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} - '@formatjs/icu-skeleton-parser@2.0.8': - resolution: {integrity: sha512-Z493tGxtKu0xNcSZjS8HrWNfq25HMscqbq5qwRFBYz14b70k1DHmhqVAwYDdDK0Ytj9YG1nvY4+IRq53LVNFdA==} + '@formatjs/icu-skeleton-parser@2.1.1': + resolution: {integrity: sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==} '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - '@formatjs/intl-localematcher@0.7.5': - resolution: {integrity: sha512-7/nd90cn5CT7SVF71/ybUKAcnvBlr9nZlJJp8O8xIZHXFgYOC4SXExZlSdgHv2l6utjw1byidL06QzChvQMHwA==} + '@formatjs/intl-localematcher@0.8.1': + resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==} '@fortawesome/fontawesome-common-types@7.1.0': resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==} @@ -3019,89 +2890,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -3129,16 +3016,13 @@ packages: '@immich/justified-layout-wasm@0.4.3': resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==} - '@immich/sdk@file:open-api/typescript-sdk': - resolution: {directory: open-api/typescript-sdk, type: directory} - '@immich/svelte-markdown-preprocess@0.2.1': resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==} peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.61.4': - resolution: {integrity: sha512-32nrY7GW6BdBQ12ZI/E4VgrgY40Yn2K31vSO6GPiOvmNgt8h3s3TSKUXbh7pY4yJgfnr7f2QL2EYRV9KkjRybQ==} + '@immich/ui@0.62.1': + resolution: {integrity: sha512-+rZAjw24pAIJ1hmCtYF16BECh+7M09UudTPc28z6U2J3CZzSOs0+Nsz5fTs8SE5wyC45QKdPWJCS//xFMrrRUg==} peerDependencies: svelte: ^5.0.0 @@ -3146,10 +3030,6 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} - '@inquirer/ansi@2.0.3': - resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - '@inquirer/checkbox@4.3.2': resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} engines: {node: '>=18'} @@ -3159,15 +3039,6 @@ packages: '@types/node': optional: true - '@inquirer/checkbox@5.0.4': - resolution: {integrity: sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/confirm@5.1.21': resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} @@ -3177,15 +3048,6 @@ packages: '@types/node': optional: true - '@inquirer/confirm@6.0.4': - resolution: {integrity: sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/core@10.3.2': resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} @@ -3195,15 +3057,6 @@ packages: '@types/node': optional: true - '@inquirer/core@11.1.1': - resolution: {integrity: sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/editor@4.2.23': resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} engines: {node: '>=18'} @@ -3213,15 +3066,6 @@ packages: '@types/node': optional: true - '@inquirer/editor@5.0.4': - resolution: {integrity: sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/expand@4.0.23': resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} engines: {node: '>=18'} @@ -3231,15 +3075,6 @@ packages: '@types/node': optional: true - '@inquirer/expand@5.0.4': - resolution: {integrity: sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -3249,23 +3084,10 @@ packages: '@types/node': optional: true - '@inquirer/external-editor@2.0.3': - resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/figures@2.0.3': - resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - '@inquirer/input@4.3.1': resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} engines: {node: '>=18'} @@ -3275,15 +3097,6 @@ packages: '@types/node': optional: true - '@inquirer/input@5.0.4': - resolution: {integrity: sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/number@3.0.23': resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} engines: {node: '>=18'} @@ -3293,15 +3106,6 @@ packages: '@types/node': optional: true - '@inquirer/number@4.0.4': - resolution: {integrity: sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/password@4.0.23': resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} @@ -3311,9 +3115,9 @@ packages: '@types/node': optional: true - '@inquirer/password@5.0.4': - resolution: {integrity: sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -3329,15 +3133,6 @@ packages: '@types/node': optional: true - '@inquirer/prompts@8.2.0': - resolution: {integrity: sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/rawlist@4.1.11': resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} engines: {node: '>=18'} @@ -3347,15 +3142,6 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@5.2.0': - resolution: {integrity: sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/search@3.2.2': resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} engines: {node: '>=18'} @@ -3365,15 +3151,6 @@ packages: '@types/node': optional: true - '@inquirer/search@4.1.0': - resolution: {integrity: sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/select@4.4.2': resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} engines: {node: '>=18'} @@ -3383,15 +3160,6 @@ packages: '@types/node': optional: true - '@inquirer/select@5.0.4': - resolution: {integrity: sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/type@3.0.10': resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} @@ -3401,15 +3169,6 @@ packages: '@types/node': optional: true - '@inquirer/type@4.0.3': - resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@internationalized/date@3.10.0': resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} @@ -3420,8 +3179,8 @@ packages: resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + '@isaacs/brace-expansion@5.0.1': + resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} engines: {node: 20 || >=22} '@isaacs/cliui@8.0.2': @@ -3603,15 +3362,18 @@ packages: resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} engines: {node: '>=6.0.0'} + '@maplibre/geojson-vt@5.0.4': + resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==} + '@maplibre/maplibre-gl-style-spec@24.4.1': resolution: {integrity: sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==} hasBin: true - '@maplibre/mlt@1.1.2': - resolution: {integrity: sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==} + '@maplibre/mlt@1.1.6': + resolution: {integrity: sha512-rgtY3x65lrrfXycLf6/T22ZnjTg5WgIOsptOIoCaMZy4O4UAKTyZlYY0h6v8le721pTptF94U65yMDQkug+URw==} - '@maplibre/vt-pbf@4.2.0': - resolution: {integrity: sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==} + '@maplibre/vt-pbf@4.2.1': + resolution: {integrity: sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA==} '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} @@ -3689,8 +3451,8 @@ packages: '@nestjs/core': ^10.0.0 || ^11.0.0 bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 - '@nestjs/cli@11.0.15': - resolution: {integrity: sha512-4Sw4i+PRI1CGVnl3F15GWytFYD+QHs6vsayVeqDhhWwL1a7ZhQyUYvmlCMoWi77rZA0+m3ObUO1WujtkXsYBDQ==} + '@nestjs/cli@11.0.16': + resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: @@ -3702,8 +3464,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.12': - resolution: {integrity: sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==} + '@nestjs/common@11.1.13': + resolution: {integrity: sha512-ieqWtipT+VlyDWLz5Rvz0f3E5rXcVAnaAi+D53DEHLjc1kmFxCgZ62qVfTX2vwkywwqNkTNXvBgGR72hYqV//Q==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -3715,8 +3477,8 @@ packages: class-validator: optional: true - '@nestjs/core@11.1.12': - resolution: {integrity: sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==} + '@nestjs/core@11.1.13': + resolution: {integrity: sha512-Tq9EIKiC30EBL8hLK93tNqaToy0hzbuVGYt29V8NhkVJUsDzlmiVf6c3hSPtzx2krIUVbTgQ2KFeaxr72rEyzQ==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -3746,21 +3508,21 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.1.12': - resolution: {integrity: sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==} + '@nestjs/platform-express@11.1.13': + resolution: {integrity: sha512-LYmi43BrAs1n74kLCUfXcHag7s1CmGETcFbf9IVyA/KWXAuAH95G3wEaZZiyabOLFNwq4ifnRGnIwUwW7cz3+w==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/platform-socket.io@11.1.12': - resolution: {integrity: sha512-1itTTYsAZecrq2NbJOkch32y8buLwN7UpcNRdJrhlS+ovJ5GxLx3RyJ3KylwBhbYnO5AeYyL1U/i4W5mg/4qDA==} + '@nestjs/platform-socket.io@11.1.13': + resolution: {integrity: sha512-04Rh16IopZzHRXt0ZjFASqt9oNFV/0m0NsYe4kVOSaTEoef3cH7cTFpNpHsfNHcc4QpYL963XE8SvIRcZs5L8A==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 rxjs: ^7.1.0 - '@nestjs/schedule@6.1.0': - resolution: {integrity: sha512-W25Ydc933Gzb1/oo7+bWzzDiOissE+h/dhIAPugA39b9MuIzBbLybuXpc1AjoQLczO3v0ldmxaffVl87W0uqoQ==} + '@nestjs/schedule@6.1.1': + resolution: {integrity: sha512-kQl1RRgi02GJ0uaUGCrXHCcwISsCsJDciCKe38ykJZgnAeeoeVWs8luWtBo4AqAAXm4nS5K8RlV0smHUJ4+2FA==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 @@ -3770,8 +3532,8 @@ packages: peerDependencies: typescript: '>=4.8.2' - '@nestjs/swagger@11.2.5': - resolution: {integrity: sha512-wCykbEybMqiYcvkyzPW4SbXKcwra9AGdajm0MvFgKR3W+gd1hfeKlo67g/s9QCRc/mqUU4KOE5Qtk7asMeFuiA==} + '@nestjs/swagger@11.2.6': + resolution: {integrity: sha512-oiXOxMQqDFyv1AKAqFzSo6JPvMEs4uA36Eyz/s2aloZLxUjcLfUMELSLSNQunr61xCPTpwEOShfmO7NIufKXdA==} peerDependencies: '@fastify/static': ^8.0.0 || ^9.0.0 '@nestjs/common': ^11.0.1 @@ -3787,8 +3549,8 @@ packages: class-validator: optional: true - '@nestjs/testing@11.1.12': - resolution: {integrity: sha512-W0M/i5nb9qRQpTQfJm+1mGT/+y4YezwwdcD7mxFG8JEZ5fz/ZEAk1Ayri2VBJKJUdo20B1ggnvqew4dlTMrSNg==} + '@nestjs/testing@11.1.13': + resolution: {integrity: sha512-bOWP8nLEZAOEEX8jAZGBCc1yU0+nv4g2ipc+QEzkVUe3eEEUKHKaeGafJ3GtDuGavlZKfkXEqflZuICdavu5dQ==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3800,8 +3562,8 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/websockets@11.1.12': - resolution: {integrity: sha512-ulSOYcgosx1TqY425cRC5oXtAu1R10+OSmVfgyR9ueR25k4luekURt8dzAZxhxSCI0OsDj9WKCFLTkEuAwg0wg==} + '@nestjs/websockets@11.1.13': + resolution: {integrity: sha512-8r8EadqBkrTYtH2uog42HfIb5fcP5a3iXymH/ityd9bO/gDson5Q1qbtCQRjuU++6NY12YYteKRu4eP/iErbLw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3844,94 +3606,94 @@ packages: '@oazapfts/runtime@1.1.0': resolution: {integrity: sha512-PwCn69pexqg/uhc0bpEHSlRFdfTtSnq3icXHd0wf4BQwZSMKsCerTnydzegVScEegYkokzIxMcl9li7on86A2w==} - '@opentelemetry/api-logs@0.210.0': - resolution: {integrity: sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==} + '@opentelemetry/api-logs@0.211.0': + resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.210.0': - resolution: {integrity: sha512-tM0ROS/hZM72kB55cSjDcghVcUXBJdGkGzpkhD7M1B/gpcvZPSGfjFgKN3dgmxNgF76NxtbUwv3ik0wS+Kz52g==} + '@opentelemetry/configuration@0.211.0': + resolution: {integrity: sha512-PNsCkzsYQKyv8wiUIsH+loC4RYyblOaDnVASBtKS22hK55ToWs2UP6IsrcfSWWn54wWTvVe2gnfwz67Pvrxf2Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks@2.4.0': - resolution: {integrity: sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==} + '@opentelemetry/context-async-hooks@2.5.0': + resolution: {integrity: sha512-uOXpVX0ZjO7heSVjhheW2XEPrhQAWr2BScDPoZ9UDycl5iuHG+Usyc3AIfG6kZeC1GyLpMInpQ6X5+9n69yOFw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.4.0': - resolution: {integrity: sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==} + '@opentelemetry/core@2.5.0': + resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.210.0': - resolution: {integrity: sha512-+BolenqOO6ow65go7uWRYPvvs/BBIWp1mtRn93VvGduqvMVH/IY8nXrt80a4L9hZ7lHi2Tq2/NcC3H2QzcWKag==} + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0': + resolution: {integrity: sha512-UhOoWENNqyaAMP/dL1YXLkXt6ZBtovkDDs1p4rxto9YwJX1+wMjwg+Obfyg2kwpcMoaiIFT3KQIcLNW8nNGNfQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.210.0': - resolution: {integrity: sha512-Q8/SEQtgrErbVVRg9M9iaG8m5wdPNdU0UOF7U43sAhwfmPG92ZOk/aenKhg0DXSNJHhkCDNCgS1kSoErAB3z0A==} + '@opentelemetry/exporter-logs-otlp-http@0.211.0': + resolution: {integrity: sha512-c118Awf1kZirHkqxdcF+rF5qqWwNjJh+BB1CmQvN9AQHC/DUIldy6dIkJn3EKlQnQ3HmuNRKc/nHHt5IusN7mA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.210.0': - resolution: {integrity: sha512-Y/yPc+gDhsWB7AsNzQWxblw4ULbvhCycMaQ2aAn+HSAVbgbMiZa0SbclPVHSnpnNzKSLVavFjweAr0pQA1KKLg==} + '@opentelemetry/exporter-logs-otlp-proto@0.211.0': + resolution: {integrity: sha512-kMvfKMtY5vJDXeLnwhrZMEwhZ2PN8sROXmzacFU/Fnl4Z79CMrOaL7OE+5X3SObRYlDUa7zVqaXp9ZetYCxfDQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.210.0': - resolution: {integrity: sha512-pWZ/Tjrqev9rdkqe8F6A9FGddLZrjl6iRAU5LBvvRL6I3PSgG8z1xM0cESAy1jzAF4wGohnAh8rB7hHzpUOYEA==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0': + resolution: {integrity: sha512-D/U3G8L4PzZp8ot5hX9wpgbTymgtLZCiwR7heMe4LsbGV4OdctS1nfyvaQHLT6CiGZ6FjKc1Vk9s6kbo9SWLXQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.210.0': - resolution: {integrity: sha512-JpLThG8Hh8A/Jzdzw9i4Ftu+EzvLaX/LouN+mOOHmadL0iror0Qsi3QWzucXeiUsDDsiYgjfKyi09e6sltytgA==} + '@opentelemetry/exporter-metrics-otlp-http@0.211.0': + resolution: {integrity: sha512-lfHXElPAoDSPpPO59DJdN5FLUnwi1wxluLTWQDayqrSPfWRnluzxRhD+g7rF8wbj1qCz0sdqABl//ug1IZyWvA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.210.0': - resolution: {integrity: sha512-CFa7SOinYOVWIWJuQL7XFeyedzmFGIpHpSMNFE8Xefb6iGB4m+MukQecdssvPcJKYlfF5FpovEOLXwafAzsXWQ==} + '@opentelemetry/exporter-metrics-otlp-proto@0.211.0': + resolution: {integrity: sha512-61iNbffEpyZv/abHaz3BQM3zUtA2kVIDBM+0dS9RK68ML0QFLRGYa50xVMn2PYMToyfszEPEgFC3ypGae2z8FA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.210.0': - resolution: {integrity: sha512-8i+7d70Hho6pcheTtbqIuS+bo+AIX/oNUTMwIEZoehUE4ZdbGmeVaE+hJS2LAErFeFaU71w164lAgYyMUEQ8zw==} + '@opentelemetry/exporter-prometheus@0.211.0': + resolution: {integrity: sha512-cD0WleEL3TPqJbvxwz5MVdVJ82H8jl8mvMad4bNU24cB5SH2mRW5aMLDTuV4614ll46R//R3RMmci26mc2L99g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.210.0': - resolution: {integrity: sha512-1GPLOyxIfUX24WM8Oea+vx9d9TlewposUnsQXTjusxVMQ/dWvt5JIDJyTsfNDS412XRUOORgF97PwsfDY5QKGA==} + '@opentelemetry/exporter-trace-otlp-grpc@0.211.0': + resolution: {integrity: sha512-eFwx4Gvu6LaEiE1rOd4ypgAiWEdZu7Qzm2QNN2nJqPW1XDeAVH1eNwVcVQl+QK9HR/JCDZ78PZgD7xD/DBDqbw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.210.0': - resolution: {integrity: sha512-9JkyaCl70anEtuKZdoCQmjDuz1/paEixY/DWfsvHt7PGKq3t8/nQ/6/xwxHjG+SkPAUbo1Iq4h7STe7Pk2bc5A==} + '@opentelemetry/exporter-trace-otlp-http@0.211.0': + resolution: {integrity: sha512-F1Rv3JeMkgS//xdVjbQMrI3+26e5SXC7vXA6trx8SWEA0OUhw4JHB+qeHtH0fJn46eFItrYbL5m8j4qi9Sfaxw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.210.0': - resolution: {integrity: sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg==} + '@opentelemetry/exporter-trace-otlp-proto@0.211.0': + resolution: {integrity: sha512-DkjXwbPiqpcPlycUojzG2RmR0/SIK8Gi9qWO9znNvSqgzrnAIE9x2n6yPfpZ+kWHZGafvsvA1lVXucTyyQa5Kg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.4.0': - resolution: {integrity: sha512-qpiXY0TUEFjBBp9b1na9LfuVQw6W8LH+te7uv+CC+0Up78ZDtZZwOjK2M7CL7Nspnw+yS4JdgEA7oxsBu0Ctsg==} + '@opentelemetry/exporter-zipkin@2.5.0': + resolution: {integrity: sha512-bk9VJgFgUAzkZzU8ZyXBSWiUGLOM3mZEgKJ1+jsZclhRnAoDNf+YBdq+G9R3cP0+TKjjWad+vVrY/bE/vRR9lA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3942,62 +3704,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.210.0': - resolution: {integrity: sha512-dICO+0D0VBnrDOmDXOvpmaP0gvai6hNhJ5y6+HFutV0UoXc7pMgJlJY3O7AzT725cW/jP38ylmfHhQa7M0Nhww==} + '@opentelemetry/instrumentation-http@0.211.0': + resolution: {integrity: sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.58.0': - resolution: {integrity: sha512-2tEJFeoM465A0FwPB0+gNvdM/xPBRIqNtC4mW+mBKy+ZKF9CWa7rEqv87OODGrigkEDpkH8Bs1FKZYbuHKCQNQ==} + '@opentelemetry/instrumentation-ioredis@0.59.0': + resolution: {integrity: sha512-875UxzBHWkW+P4Y45SoFM2AR8f8TzBMD8eO7QXGCyFSCUMP5s9vtt/BS8b/r2kqLyaRPK6mLbdnZznK3XzQWvw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.56.0': - resolution: {integrity: sha512-2wKd6+/nKyZVTkElTHRZAAEQ7moGqGmTIXlZvfAeV/dNA+6zbbl85JBcyeUFIYt+I42Naq5RgKtUY8fK6/GE1g==} + '@opentelemetry/instrumentation-nestjs-core@0.57.0': + resolution: {integrity: sha512-mzTjjethjuk70o/vWUeV12QwMG9EAFJpkn13/q8zi++sNosf2hoGXTplIdbs81U8S3PJ4GxHKsBjM0bj1CGZ0g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.62.0': - resolution: {integrity: sha512-/ZSMRCyFRMjQVx7Wf+BIAOMEdN/XWBbAGTNLKfQgGYs1GlmdiIFkUy8Z8XGkToMpKrgZju0drlTQpqt4Ul7R6w==} + '@opentelemetry/instrumentation-pg@0.63.0': + resolution: {integrity: sha512-dKm/ODNN3GgIQVlbD6ZPxwRc3kleLf95hrRWXM+l8wYo+vSeXtEpQPT53afEf6VFWDVzJK55VGn8KMLtSve/cg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.210.0': - resolution: {integrity: sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g==} + '@opentelemetry/instrumentation@0.211.0': + resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.210.0': - resolution: {integrity: sha512-uk78DcZoBNHIm26h0oXc8Pizh4KDJ/y04N5k/UaI9J7xR7mL8QcMcYPQG9xxN7m8qotXOMDRW6qTAyptav4+3w==} + '@opentelemetry/otlp-exporter-base@0.211.0': + resolution: {integrity: sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.210.0': - resolution: {integrity: sha512-fEJs8UhkFMrdXMOCLXyKd2uc6N209tIi8IBNqSTi83ri+MlMFrBKnOtklmv9/zzxovoN5zD1waRt6XBFGPfmIw==} + '@opentelemetry/otlp-grpc-exporter-base@0.211.0': + resolution: {integrity: sha512-mR5X+N4SuphJeb7/K7y0JNMC8N1mB6gEtjyTLv+TSAhl0ZxNQzpSKP8S5Opk90fhAqVYD4R0SQSAirEBlH1KSA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.210.0': - resolution: {integrity: sha512-nkHBJVSJGOwkRZl+BFIr7gikA93/U8XkL2EWaiDbj3DVjmTEZQpegIKk0lT8oqQYfP8FC6zWNjuTfkaBVqa0ZQ==} + '@opentelemetry/otlp-transformer@0.211.0': + resolution: {integrity: sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.4.0': - resolution: {integrity: sha512-6VPsFiMUkJBre/86F0d+PZMaUCcuLA9DtZuC46KH8EeVEKZPEM2WlX35M/qmde8UpzoQL9qzdz54YjUYABt8Uw==} + '@opentelemetry/propagator-b3@2.5.0': + resolution: {integrity: sha512-g10m4KD73RjHrSvUge+sUxUl8m4VlgnGc6OKvo68a4uMfaLjdFU+AULfvMQE/APq38k92oGUxEzBsAZ8RN/YHg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.4.0': - resolution: {integrity: sha512-t6muBL/3AMD++1EMF658C/KIpj3gfmTmftX3mEQql4KIxNGFvacCmmTtrQt9IZAJmQRfjQRCkv+vsGbQugeJIw==} + '@opentelemetry/propagator-jaeger@2.5.0': + resolution: {integrity: sha512-t70ErZCncAR/zz5AcGkL0TF25mJiK1FfDPEQCgreyAHZ+mRJ/bNUiCnImIBDlP3mSDXy6N09DbUEKq0ktW98Hg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -4006,44 +3768,44 @@ packages: resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resources@2.4.0': - resolution: {integrity: sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==} + '@opentelemetry/resources@2.5.0': + resolution: {integrity: sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.210.0': - resolution: {integrity: sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ==} + '@opentelemetry/sdk-logs@0.211.0': + resolution: {integrity: sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.4.0': - resolution: {integrity: sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==} + '@opentelemetry/sdk-metrics@2.5.0': + resolution: {integrity: sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.210.0': - resolution: {integrity: sha512-KymqUtYvfpblDNgGxBXYqCcDjYXwjOF7Muc6ocs0rMlG/66Hcs9KiJ7hg4zLOv63JubF/vxi5WXaLrQrPKyaZQ==} + '@opentelemetry/sdk-node@0.211.0': + resolution: {integrity: sha512-+s1eGjoqmPCMptNxcJJD4IxbWJKNLOQFNKhpwkzi2gLkEbCj6LzSHJNhPcLeBrBlBLtlSpibM+FuS7fjZ8SSFQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.4.0': - resolution: {integrity: sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==} + '@opentelemetry/sdk-trace-base@2.5.0': + resolution: {integrity: sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.4.0': - resolution: {integrity: sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==} + '@opentelemetry/sdk-trace-node@2.5.0': + resolution: {integrity: sha512-O6N/ejzburFm2C84aKNrwJVPpt6HSTSq8T0ZUMq3xT2XmqT4cwxUItcL5UWGThYuq8RTcbH8u1sfj6dmRci0Ow==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/semantic-conventions@1.38.0': - resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} engines: {node: '>=14'} '@opentelemetry/sql-common@0.41.2': @@ -4084,36 +3846,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -4178,8 +3946,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.57.0': - resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} hasBin: true @@ -4403,66 +4171,79 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -4529,178 +4310,6 @@ packages: '@slorber/remark-comment@1.0.0': resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} - '@smithy/abort-controller@4.2.8': - resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} - engines: {node: '>=18.0.0'} - - '@smithy/config-resolver@4.4.6': - resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.20.7': - resolution: {integrity: sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.2.8': - resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.3.9': - resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.2.8': - resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.2.8': - resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} - engines: {node: '>=18.0.0'} - - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/is-array-buffer@4.2.0': - resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-content-length@4.2.8': - resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.4.8': - resolution: {integrity: sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.4.24': - resolution: {integrity: sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.2.9': - resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.2.8': - resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.3.8': - resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.4.8': - resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==} - engines: {node: '>=18.0.0'} - - '@smithy/property-provider@4.2.8': - resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@5.3.8': - resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.2.8': - resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-parser@4.2.8': - resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} - engines: {node: '>=18.0.0'} - - '@smithy/service-error-classification@4.2.8': - resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.4.3': - resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.3.8': - resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.10.9': - resolution: {integrity: sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.12.0': - resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} - engines: {node: '>=18.0.0'} - - '@smithy/url-parser@4.2.8': - resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-base64@4.3.0': - resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.2.0': - resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.2.1': - resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-buffer-from@4.2.0': - resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.2.0': - resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.3.23': - resolution: {integrity: sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.2.26': - resolution: {integrity: sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==} - engines: {node: '>=18.0.0'} - - '@smithy/util-endpoints@3.2.8': - resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-hex-encoding@4.2.0': - resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.2.8': - resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.2.8': - resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-stream@4.5.10': - resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.2.0': - resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@4.2.0': - resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} - engines: {node: '>=18.0.0'} - - '@smithy/uuid@1.1.0': - resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} - engines: {node: '>=18.0.0'} - '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -4716,8 +4325,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.8': - resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} peerDependencies: acorn: ^8.9.0 @@ -4726,15 +4335,15 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/enhanced-img@0.9.2': - resolution: {integrity: sha512-hAYZ8YFgYtqrQ0dXyq6rdmHBFyG+eIQnNjdIoVhqeZQEBIREXoBThkx+7FtDa6ZV35lTRaT9dgFKF4W+4LbuaQ==} + '@sveltejs/enhanced-img@0.10.0': + resolution: {integrity: sha512-+nSrtNfs2dgKQ6RHMoKO6chl1QoO8JsuwKHkj9LkA2fUwzDYkeYoWvJzddOJIbgmowMdhi9cLo6tckSU+Kk7DQ==} peerDependencies: '@sveltejs/vite-plugin-svelte': ^6.0.0 svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.49.5': - resolution: {integrity: sha512-dCYqelr2RVnWUuxc+Dk/dB/SjV/8JBndp1UovCyCZdIQezd8TRwFLNZctYkzgHxRJtaNvseCSRsuuHPeUgIN/A==} + '@sveltejs/kit@2.50.2': + resolution: {integrity: sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -4842,68 +4451,72 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.15.8': - resolution: {integrity: sha512-M9cK5GwyWWRkRGwwCbREuj6r8jKdES/haCZ3Xckgkl8MUQJZA3XB7IXXK1IXRNeLjg6m7cnoMICpXv1v1hlJOg==} + '@swc/core-darwin-arm64@1.15.11': + resolution: {integrity: sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.8': - resolution: {integrity: sha512-j47DasuOvXl80sKJHSi2X25l44CMc3VDhlJwA7oewC1nV1VsSzwX+KOwE5tLnfORvVJJyeiXgJORNYg4jeIjYQ==} + '@swc/core-darwin-x64@1.15.11': + resolution: {integrity: sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.8': - resolution: {integrity: sha512-siAzDENu2rUbwr9+fayWa26r5A9fol1iORG53HWxQL1J8ym4k7xt9eME0dMPXlYZDytK5r9sW8zEA10F2U3Xwg==} + '@swc/core-linux-arm-gnueabihf@1.15.11': + resolution: {integrity: sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.8': - resolution: {integrity: sha512-o+1y5u6k2FfPYbTRUPvurwzNt5qd0NTumCTFscCNuBksycloXY16J8L+SMW5QRX59n4Hp9EmFa3vpvNHRVv1+Q==} + '@swc/core-linux-arm64-gnu@1.15.11': + resolution: {integrity: sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.8': - resolution: {integrity: sha512-koiCqL09EwOP1S2RShCI7NbsQuG6r2brTqUYE7pV7kZm9O17wZ0LSz22m6gVibpwEnw8jI3IE1yYsQTVpluALw==} + '@swc/core-linux-arm64-musl@1.15.11': + resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] - '@swc/core-linux-x64-gnu@1.15.8': - resolution: {integrity: sha512-4p6lOMU3bC+Vd5ARtKJ/FxpIC5G8v3XLoPEZ5s7mLR8h7411HWC/LmTXDHcrSXRC55zvAVia1eldy6zDLz8iFQ==} + '@swc/core-linux-x64-gnu@1.15.11': + resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] - '@swc/core-linux-x64-musl@1.15.8': - resolution: {integrity: sha512-z3XBnbrZAL+6xDGAhJoN4lOueIxC/8rGrJ9tg+fEaeqLEuAtHSW2QHDHxDwkxZMjuF/pZ6MUTjHjbp8wLbuRLA==} + '@swc/core-linux-x64-musl@1.15.11': + resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.8': - resolution: {integrity: sha512-djQPJ9Rh9vP8GTS/Df3hcc6XP6xnG5c8qsngWId/BLA9oX6C7UzCPAn74BG/wGb9a6j4w3RINuoaieJB3t+7iQ==} + '@swc/core-win32-arm64-msvc@1.15.11': + resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.8': - resolution: {integrity: sha512-/wfAgxORg2VBaUoFdytcVBVCgf1isWZIEXB9MZEUty4wwK93M/PxAkjifOho9RN3WrM3inPLabICRCEgdHpKKQ==} + '@swc/core-win32-ia32-msvc@1.15.11': + resolution: {integrity: sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.8': - resolution: {integrity: sha512-GpMePrh9Sl4d61o4KAHOOv5is5+zt6BEXCOCgs/H0FLGeii7j9bWDE8ExvKFy2GRRZVNR1ugsnzaGWHKM6kuzA==} + '@swc/core-win32-x64-msvc@1.15.11': + resolution: {integrity: sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.8': - resolution: {integrity: sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==} + '@swc/core@1.15.11': + resolution: {integrity: sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4962,24 +4575,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -5280,9 +4897,6 @@ packages: '@types/fluent-ffmpeg@2.1.28': resolution: {integrity: sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==} - '@types/geojson-vt@3.2.5': - resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} - '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -5394,17 +5008,14 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.30': - resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} + '@types/node@24.10.13': + resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} - '@types/node@24.10.9': - resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} + '@types/node@25.2.3': + resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} - '@types/node@25.0.9': - resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} - - '@types/nodemailer@7.0.5': - resolution: {integrity: sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==} + '@types/nodemailer@7.0.9': + resolution: {integrity: sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==} '@types/oidc-provider@9.5.0': resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} @@ -5448,8 +5059,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.2.8': - resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} + '@types/react@19.2.13': + resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -5532,63 +5143,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.53.0': - resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} + '@typescript-eslint/eslint-plugin@8.54.0': + resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.53.0 + '@typescript-eslint/parser': ^8.54.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.53.0': - resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==} + '@typescript-eslint/parser@8.54.0': + resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.53.0': - resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==} + '@typescript-eslint/project-service@8.54.0': + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.53.0': - resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==} + '@typescript-eslint/scope-manager@8.54.0': + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.53.0': - resolution: {integrity: sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==} + '@typescript-eslint/tsconfig-utils@8.54.0': + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.53.0': - resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==} + '@typescript-eslint/type-utils@8.54.0': + resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.53.0': - resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==} + '@typescript-eslint/types@8.54.0': + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.53.0': - resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==} + '@typescript-eslint/typescript-estree@8.54.0': + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.53.0': - resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==} + '@typescript-eslint/utils@8.54.0': + resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.53.0': - resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} + '@typescript-eslint/visitor-keys@8.54.0': + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -5687,11 +5298,11 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@zoom-image/core@0.41.4': - resolution: {integrity: sha512-zUJNHWQzx8rmfNOlp2Rr0+n8I7QK9hLNThnusdtvz20/HN+J//RcDJmCuRDj6jUW/qJGh9FWR5sROMFBuPLPfQ==} + '@zoom-image/core@0.42.0': + resolution: {integrity: sha512-aF7siQqxqmOVlBd65deaCM7L/6V80Rp7HazZJpxtErh8zAn5itXXKBv1KA1NufSPfRZsXl1QtysxkjB3gVIzxw==} - '@zoom-image/svelte@0.3.8': - resolution: {integrity: sha512-rkXS+JS4qkBccmRK9+I5j+Pe4rp78GWK/7y0EduBJNtt38q+AwmKhhQs8oTMKTU6lOzLgxjXy1TI802mtvcAmw==} + '@zoom-image/svelte@0.3.9': + resolution: {integrity: sha512-27Nze2f0W7Jop12imiWYvZGqiAlmQbBCqMVJPtUvmaBdv2KY4BhrSe4k7pBJaQId5dMF9SwUPo7obrtm9dCzuQ==} peerDependencies: svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -5939,8 +5550,8 @@ packages: autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} - autoprefixer@10.4.23: - resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + autoprefixer@10.4.24: + resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -6037,8 +5648,8 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.9.7: - resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true batch-cluster@16.0.0: @@ -6089,9 +5700,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.13.1: - resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} - boxen@6.2.1: resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6139,8 +5747,8 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} - bullmq@5.66.5: - resolution: {integrity: sha512-DC1E7P03L+TfNHv+2SGxwNYvtb0oJPODWSKkWdfis0heU5zFW16vjM7fCjwlxMdGWw2w28EI3mTRfYLEHeQQSw==} + bullmq@5.67.3: + resolution: {integrity: sha512-eeQobOJn8M0Rj8tcZCVFLrimZgJQallJH1JpclOoyut2nDNkDwTEPMVcZzLeSR2fGeIVbfJTjU96F563Qkge5A==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -6225,8 +5833,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001760: - resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} canvas@2.11.2: resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} @@ -6606,8 +6214,8 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} cose-base@1.0.3: @@ -6645,8 +6253,8 @@ packages: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} - cron@4.3.5: - resolution: {integrity: sha512-hKPP7fq1+OfyCqoePkKfVq7tNAdFwiQORr4lZUHwrf0tebC65fYEeWgOrXOL6prn1/fegGOdTfrM6e34PJfksg==} + cron@4.4.0: + resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} engines: {node: '>=18.x'} cross-spawn@7.0.6: @@ -7229,8 +6837,8 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + dotenv@17.2.4: + resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -7255,8 +6863,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -7298,8 +6906,8 @@ packages: resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} entities@2.2.0: @@ -7375,8 +6983,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true @@ -7414,8 +7022,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-compat@6.0.2: - resolution: {integrity: sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA==} + eslint-plugin-compat@6.1.0: + resolution: {integrity: sha512-xiwHz7mj6+Zj7NWOO/uaWdrQ6zP0zL5CPyKVCNlB4JaoUFeYPYwejf5toqyHGlXzhuPUdCpg31uBRiWqcgiS0A==} engines: {node: '>=18.x'} peerDependencies: eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -7496,8 +7104,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@2.2.1: - resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} + esrap@2.2.3: + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -7660,10 +7268,6 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@5.2.5: - resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} - hasBin: true - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -7877,9 +7481,6 @@ packages: geojson-vt@3.2.1: resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} - geojson-vt@4.0.2: - resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} - geojson@0.5.0: resolution: {integrity: sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==} engines: {node: '>= 0.10'} @@ -7939,20 +7540,26 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.0: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} + glob@13.0.2: + resolution: {integrity: sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ==} + engines: {node: 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -8020,8 +7627,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.3.0: - resolution: {integrity: sha512-5qJbkqcvR8j/a4av5IWqqIWmEGf9dt6OhGMS6qxCgjSOBGzGa5XLoqg40OyD8XNzQ+g1g2zsXi10kjfpzYH55Q==} + happy-dom@20.5.0: + resolution: {integrity: sha512-VQe+Q5CYiGOgcCERXhcfNsbnrN92FDEKciMH/x6LppU9dd0j4aTjCTlqONFOIMcAm/5JxS3+utowbXV1OoFr+g==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -8350,14 +7957,14 @@ packages: intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - intl-messageformat@11.0.9: - resolution: {integrity: sha512-xA4aCCMnCxynKV5kI7V0GlMf+BGJxsXQRwr5tfEgmcB791eDEQa4r+s4wU7GqMR0jx7+K4jyEH2UfBpVGTDNPQ==} + intl-messageformat@11.1.2: + resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@5.9.1: - resolution: {integrity: sha512-BXNqFQ66oOsR82g9ajFFsR8ZKrjVvYCLyeML9IvSMAsP56XH2VXBdZjmI11p65nXXJxTEt1hie3J2QeFJVgrtQ==} + ioredis@5.9.2: + resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} engines: {node: '>=12.22.0'} ip-address@10.1.0: @@ -8553,9 +8160,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} @@ -8739,7 +8346,6 @@ packages: keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -8853,24 +8459,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -8978,9 +8588,6 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} @@ -9019,8 +8626,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -9068,8 +8675,8 @@ packages: resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} engines: {node: '>=6.4.0'} - maplibre-gl@5.16.0: - resolution: {integrity: sha512-/VDY89nr4jgLJyzmhy325cG6VUI02WkZ/UfVuDbG/piXzo6ODnM+omDFIwWY8tsEsBG26DNDmNMn3Y2ikHsBiA==} + maplibre-gl@5.17.0: + resolution: {integrity: sha512-gwS6NpXBfWD406dtT5YfEpl2hmpMm+wcPqf04UAez/TxY1OBjiMdK2ZoMGcNIlGHelKc4+Uet6zhDdDEnlJVHA==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} mark.js@8.11.1: @@ -9404,8 +9011,8 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + minimatch@10.1.2: + resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} engines: {node: 20 || >=22} minimatch@3.1.2: @@ -9426,8 +9033,8 @@ packages: resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} engines: {node: '>=16 || 14 >=14.17'} - minipass-fetch@5.0.0: - resolution: {integrity: sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==} + minipass-fetch@5.0.1: + resolution: {integrity: sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==} engines: {node: ^20.17.0 || >=22.9.0} minipass-flush@1.0.5: @@ -9438,8 +9045,8 @@ packages: resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} engines: {node: '>=8'} - minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} engines: {node: '>=8'} minipass@3.3.6: @@ -9533,10 +9140,6 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -9656,16 +9259,16 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-gyp@12.1.0: - resolution: {integrity: sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==} + node-gyp@12.2.0: + resolution: {integrity: sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - nodemailer@7.0.12: - resolution: {integrity: sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==} + nodemailer@7.0.13: + resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} engines: {node: '>=6.0.0'} nopt@1.0.10: @@ -9983,8 +9586,8 @@ packages: pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - pg-connection-string@2.10.0: - resolution: {integrity: sha512-ur/eoPKzDx2IjPaYyXS6Y8NSblxM7X64deV2ObV57vhjsWiwLvUD6meukAzogiOsu60GO8m/3Cb6FdJsWNjwXg==} + pg-connection-string@2.11.0: + resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} @@ -10002,8 +9605,8 @@ packages: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.17.1: - resolution: {integrity: sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==} + pg@8.18.0: + resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -10047,13 +9650,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} hasBin: true - playwright@1.57.0: - resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} engines: {node: '>=18'} hasBin: true @@ -10064,8 +9667,8 @@ packages: pmtiles@3.2.1: resolution: {integrity: sha512-3R4fBwwoli5mw7a6t1IGwOtfmcSAODq6Okz0zkXhS1zi9sz1ssjjIfslwPvcWw5TNhdjNBUg9fgfPLeqZlH6ng==} - pmtiles@4.3.2: - resolution: {integrity: sha512-Ath2F2U2E37QyNXjN1HOF+oLiNIbdrDYrk/K3C9K4Pgw2anwQX10y4WYWEH9O75vPiu0gBbSWIAbSG19svyvZg==} + pmtiles@4.4.0: + resolution: {integrity: sha512-tCLI1C5134MR54i8izUWhse0QUtO/EC33n9yWp1N5dYLLvyc197U0fkF5gAJhq1TdWO9Tvl+9hgvFvM0fR27Zg==} pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} @@ -10591,8 +10194,8 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.8.0: - resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -10756,10 +10359,10 @@ packages: peerDependencies: react: ^18.3.1 - react-dom@19.2.3: - resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: - react: ^19.2.3 + react: ^19.2.4 react-email@4.3.2: resolution: {integrity: sha512-WaZcnv9OAIRULY236zDRdk+8r511ooJGH5UOb7FnVsV33hGPI+l5aIZ6drVjXi4QrlLTmLm8PsYvmXRSv31MPA==} @@ -10811,8 +10414,8 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -11166,6 +10769,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} engines: {node: '>= 0.8.0'} @@ -11195,8 +10803,8 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.0.1: + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -11400,8 +11008,8 @@ packages: resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} engines: {node: '>=10.16.0'} - ssri@13.0.0: - resolution: {integrity: sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} engines: {node: ^20.17.0 || >=22.9.0} stackback@0.0.2: @@ -11499,9 +11107,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@2.1.2: - resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} - strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -11560,8 +11165,8 @@ packages: peerDependencies: svelte: '>= 3.43.1 < 6' - svelte-check@4.3.5: - resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==} + svelte-check@4.3.6: + resolution: {integrity: sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -11598,8 +11203,8 @@ packages: peerDependencies: svelte: ^5.0.0 - svelte-maplibre@1.2.5: - resolution: {integrity: sha512-Uklcbi6inW9GA0MuSusbXmFr/MQPmXrjuP8hS1+yFX3ySvCQ477tsM3I7Jo/fUDK3XAxFSIHW6hZfucnM3kXwQ==} + svelte-maplibre@1.2.6: + resolution: {integrity: sha512-NntxiZptS07HwblUxIkDllAeBSj6DTyEtECkOqxEi3e/uam7Qunkd/Cp535NN1K7eIx5MLs4cyAa8jgPDgGLFw==} peerDependencies: '@deck.gl/core': ^9 '@deck.gl/layers': ^9 @@ -11633,8 +11238,8 @@ packages: peerDependencies: svelte: ^5.30.2 - svelte@5.48.0: - resolution: {integrity: sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ==} + svelte@5.50.0: + resolution: {integrity: sha512-FR9kTLmX5i0oyeQ5j/+w8DuagIkQ7MWMuPpPVioW2zx9Dw77q+1ufLzF1IqNtcTXPRnIIio4PlasliVn43OnbQ==} engines: {node: '>=18'} svg-parser@2.0.4: @@ -11731,12 +11336,11 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - tar@7.5.2: - resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + tar@7.5.7: + resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} @@ -11999,8 +11603,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.53.0: - resolution: {integrity: sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==} + typescript-eslint@8.54.0: + resolution: {integrity: sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -12014,8 +11618,8 @@ packages: ua-is-frozen@0.1.2: resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} - ua-parser-js@2.0.8: - resolution: {integrity: sha512-BdnBM5waFormdrOFBU+cA90R689V0tWUWlIG2i30UXxElHjuCu5+dOV2Etw3547jcQ/yaLtPm9wrqIuOY2bSJg==} + ua-parser-js@2.0.9: + resolution: {integrity: sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==} hasBin: true ufo@1.6.2: @@ -12041,9 +11645,6 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -12143,8 +11744,8 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} - update-browserslist-db@1.2.2: - resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -12258,13 +11859,10 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-tsconfig-paths@6.0.4: - resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} + vite-tsconfig-paths@6.1.0: + resolution: {integrity: sha512-kpd3sY9glHIDaq4V/Tlc1Y8WaKtutoc3B525GHxEVKWX42FKfQsXvjFOemu1I8VIN8pNbrMLWVTbW79JaRUxKg==} peerDependencies: vite: '*' - peerDependenciesMeta: - vite: - optional: true vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} @@ -12516,8 +12114,8 @@ packages: engines: {node: '>= 8'} hasBin: true - which@6.0.0: - resolution: {integrity: sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==} + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true @@ -12555,10 +12153,6 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -12875,11 +12469,11 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.19(@types/node@24.10.9)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@19.2.19(@types/node@24.10.13)(chokidar@4.0.3)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) - '@inquirer/prompts': 7.3.2(@types/node@24.10.9) + '@inquirer/prompts': 7.3.2(@types/node@24.10.13) ansi-colors: 4.1.3 symbol-observable: 4.0.0 yargs-parser: 21.1.1 @@ -12921,405 +12515,7 @@ snapshots: lru-cache: 10.4.3 optional: true - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.969.0 - '@aws-sdk/util-locate-window': 3.965.2 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.969.0 - tslib: 2.8.1 - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-sesv2@3.971.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.970.0 - '@aws-sdk/credential-provider-node': 3.971.0 - '@aws-sdk/middleware-host-header': 3.969.0 - '@aws-sdk/middleware-logger': 3.969.0 - '@aws-sdk/middleware-recursion-detection': 3.969.0 - '@aws-sdk/middleware-user-agent': 3.970.0 - '@aws-sdk/region-config-resolver': 3.969.0 - '@aws-sdk/signature-v4-multi-region': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@aws-sdk/util-endpoints': 3.970.0 - '@aws-sdk/util-user-agent-browser': 3.969.0 - '@aws-sdk/util-user-agent-node': 3.971.0 - '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.20.7 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.8 - '@smithy/middleware-retry': 4.4.24 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.23 - '@smithy/util-defaults-mode-node': 4.2.26 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-sso@3.971.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.970.0 - '@aws-sdk/middleware-host-header': 3.969.0 - '@aws-sdk/middleware-logger': 3.969.0 - '@aws-sdk/middleware-recursion-detection': 3.969.0 - '@aws-sdk/middleware-user-agent': 3.970.0 - '@aws-sdk/region-config-resolver': 3.969.0 - '@aws-sdk/types': 3.969.0 - '@aws-sdk/util-endpoints': 3.970.0 - '@aws-sdk/util-user-agent-browser': 3.969.0 - '@aws-sdk/util-user-agent-node': 3.971.0 - '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.20.7 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.8 - '@smithy/middleware-retry': 4.4.24 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.23 - '@smithy/util-defaults-mode-node': 4.2.26 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.970.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@aws-sdk/xml-builder': 3.969.0 - '@smithy/core': 3.20.7 - '@smithy/node-config-provider': 4.3.8 - '@smithy/property-provider': 4.2.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/signature-v4': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/util-base64': 4.3.0 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.970.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@smithy/property-provider': 4.2.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.970.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/node-http-handler': 4.4.8 - '@smithy/property-provider': 4.2.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/util-stream': 4.5.10 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.971.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/credential-provider-env': 3.970.0 - '@aws-sdk/credential-provider-http': 3.970.0 - '@aws-sdk/credential-provider-login': 3.971.0 - '@aws-sdk/credential-provider-process': 3.970.0 - '@aws-sdk/credential-provider-sso': 3.971.0 - '@aws-sdk/credential-provider-web-identity': 3.971.0 - '@aws-sdk/nested-clients': 3.971.0 - '@aws-sdk/types': 3.969.0 - '@smithy/credential-provider-imds': 4.2.8 - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-login@3.971.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/nested-clients': 3.971.0 - '@aws-sdk/types': 3.969.0 - '@smithy/property-provider': 4.2.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.971.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.970.0 - '@aws-sdk/credential-provider-http': 3.970.0 - '@aws-sdk/credential-provider-ini': 3.971.0 - '@aws-sdk/credential-provider-process': 3.970.0 - '@aws-sdk/credential-provider-sso': 3.971.0 - '@aws-sdk/credential-provider-web-identity': 3.971.0 - '@aws-sdk/types': 3.969.0 - '@smithy/credential-provider-imds': 4.2.8 - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-process@3.970.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.971.0': - dependencies: - '@aws-sdk/client-sso': 3.971.0 - '@aws-sdk/core': 3.970.0 - '@aws-sdk/token-providers': 3.971.0 - '@aws-sdk/types': 3.969.0 - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-web-identity@3.971.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/nested-clients': 3.971.0 - '@aws-sdk/types': 3.969.0 - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/middleware-host-header@3.969.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-logger@3.969.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-recursion-detection@3.969.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@aws/lambda-invoke-store': 0.2.3 - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-sdk-s3@3.970.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@aws-sdk/util-arn-parser': 3.968.0 - '@smithy/core': 3.20.7 - '@smithy/node-config-provider': 4.3.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/signature-v4': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/util-config-provider': 4.2.0 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-stream': 4.5.10 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-user-agent@3.970.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@aws-sdk/util-endpoints': 3.970.0 - '@smithy/core': 3.20.7 - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.971.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.970.0 - '@aws-sdk/middleware-host-header': 3.969.0 - '@aws-sdk/middleware-logger': 3.969.0 - '@aws-sdk/middleware-recursion-detection': 3.969.0 - '@aws-sdk/middleware-user-agent': 3.970.0 - '@aws-sdk/region-config-resolver': 3.969.0 - '@aws-sdk/types': 3.969.0 - '@aws-sdk/util-endpoints': 3.970.0 - '@aws-sdk/util-user-agent-browser': 3.969.0 - '@aws-sdk/util-user-agent-node': 3.971.0 - '@smithy/config-resolver': 4.4.6 - '@smithy/core': 3.20.7 - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/hash-node': 4.2.8 - '@smithy/invalid-dependency': 4.2.8 - '@smithy/middleware-content-length': 4.2.8 - '@smithy/middleware-endpoint': 4.4.8 - '@smithy/middleware-retry': 4.4.24 - '@smithy/middleware-serde': 4.2.9 - '@smithy/middleware-stack': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/node-http-handler': 4.4.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.23 - '@smithy/util-defaults-mode-node': 4.2.26 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/region-config-resolver@3.969.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@smithy/config-resolver': 4.4.6 - '@smithy/node-config-provider': 4.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.970.0': - dependencies: - '@aws-sdk/middleware-sdk-s3': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@smithy/protocol-http': 5.3.8 - '@smithy/signature-v4': 5.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.971.0': - dependencies: - '@aws-sdk/core': 3.970.0 - '@aws-sdk/nested-clients': 3.971.0 - '@aws-sdk/types': 3.969.0 - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.969.0': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/util-arn-parser@3.968.0': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.970.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-endpoints': 3.2.8 - tslib: 2.8.1 - - '@aws-sdk/util-locate-window@3.965.2': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-browser@3.969.0': - dependencies: - '@aws-sdk/types': 3.969.0 - '@smithy/types': 4.12.0 - bowser: 2.13.1 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.971.0': - dependencies: - '@aws-sdk/middleware-user-agent': 3.970.0 - '@aws-sdk/types': 3.969.0 - '@smithy/node-config-provider': 4.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.969.0': - dependencies: - '@smithy/types': 4.12.0 - fast-xml-parser: 5.2.5 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.2.3': {} - - '@babel/code-frame@7.28.6': + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 @@ -13329,7 +12525,7 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) @@ -14051,13 +13247,13 @@ snapshots: '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@babel/traverse@7.28.5': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 @@ -14451,26 +13647,26 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.3.1(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docsearch/core@4.3.1(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': optionalDependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.13 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@docsearch/css@4.3.2': {} - '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': + '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': dependencies: '@ai-sdk/react': 2.0.115(react@18.3.1)(zod@4.2.1) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.0)(algoliasearch@5.46.0)(search-insights@2.17.3) - '@docsearch/core': 4.3.1(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docsearch/core': 4.3.1(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docsearch/css': 4.3.2 ai: 5.0.113(zod@4.2.1) algoliasearch: 5.46.0 marked: 16.4.2 zod: 4.2.1 optionalDependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.13 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) search-insights: 2.17.3 @@ -14544,7 +13740,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: '@docusaurus/babel': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/bundler': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) @@ -14553,7 +13749,7 @@ snapshots: '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -14582,7 +13778,7 @@ snapshots: react-router: 5.3.4(react@18.3.1) react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) react-router-dom: 5.3.4(react@18.3.1) - semver: 7.7.3 + semver: 7.7.4 serve-handler: 6.1.6 tinypool: 1.1.1 tslib: 2.8.1 @@ -14659,7 +13855,7 @@ snapshots: dependencies: '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.8 + '@types/react': 19.2.13 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 18.3.1 @@ -14673,13 +13869,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14714,13 +13910,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14754,9 +13950,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14784,9 +13980,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14811,9 +14007,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 @@ -14839,9 +14035,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -14865,9 +14061,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gtag.js': 0.0.12 @@ -14892,9 +14088,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -14918,9 +14114,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14949,9 +14145,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14979,22 +14175,22 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -15021,25 +14217,25 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@18.3.1)': dependencies: - '@types/react': 19.2.8 + '@types/react': 19.2.13 react: 18.3.1 - '@docusaurus/theme-classic@3.9.2(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/theme-classic@3.9.2(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@18.3.1) clsx: 2.1.1 infima: 0.2.0-alpha.45 lodash: 4.17.23 @@ -15071,15 +14267,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.8 + '@types/react': 19.2.13 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -15095,11 +14291,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-mermaid@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/theme-mermaid@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) mermaid: 11.12.2 @@ -15125,13 +14321,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': dependencies: - '@docsearch/react': 4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docsearch/react': 4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15178,7 +14374,7 @@ snapshots: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 '@types/mdast': 4.0.4 - '@types/react': 19.2.8 + '@types/react': 19.2.13 commander: 5.1.0 joi: 17.13.3 react: 18.3.1 @@ -15269,7 +14465,7 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.27.2': + '@esbuild/aix-ppc64@0.27.3': optional: true '@esbuild/android-arm64@0.19.12': @@ -15278,7 +14474,7 @@ snapshots: '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.2': + '@esbuild/android-arm64@0.27.3': optional: true '@esbuild/android-arm@0.19.12': @@ -15287,7 +14483,7 @@ snapshots: '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.2': + '@esbuild/android-arm@0.27.3': optional: true '@esbuild/android-x64@0.19.12': @@ -15296,7 +14492,7 @@ snapshots: '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.27.2': + '@esbuild/android-x64@0.27.3': optional: true '@esbuild/darwin-arm64@0.19.12': @@ -15305,7 +14501,7 @@ snapshots: '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.27.2': + '@esbuild/darwin-arm64@0.27.3': optional: true '@esbuild/darwin-x64@0.19.12': @@ -15314,7 +14510,7 @@ snapshots: '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.2': + '@esbuild/darwin-x64@0.27.3': optional: true '@esbuild/freebsd-arm64@0.19.12': @@ -15323,7 +14519,7 @@ snapshots: '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.2': + '@esbuild/freebsd-arm64@0.27.3': optional: true '@esbuild/freebsd-x64@0.19.12': @@ -15332,7 +14528,7 @@ snapshots: '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.27.2': + '@esbuild/freebsd-x64@0.27.3': optional: true '@esbuild/linux-arm64@0.19.12': @@ -15341,7 +14537,7 @@ snapshots: '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.2': + '@esbuild/linux-arm64@0.27.3': optional: true '@esbuild/linux-arm@0.19.12': @@ -15350,7 +14546,7 @@ snapshots: '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.2': + '@esbuild/linux-arm@0.27.3': optional: true '@esbuild/linux-ia32@0.19.12': @@ -15359,7 +14555,7 @@ snapshots: '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.2': + '@esbuild/linux-ia32@0.27.3': optional: true '@esbuild/linux-loong64@0.19.12': @@ -15368,7 +14564,7 @@ snapshots: '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.2': + '@esbuild/linux-loong64@0.27.3': optional: true '@esbuild/linux-mips64el@0.19.12': @@ -15377,7 +14573,7 @@ snapshots: '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.2': + '@esbuild/linux-mips64el@0.27.3': optional: true '@esbuild/linux-ppc64@0.19.12': @@ -15386,7 +14582,7 @@ snapshots: '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.2': + '@esbuild/linux-ppc64@0.27.3': optional: true '@esbuild/linux-riscv64@0.19.12': @@ -15395,7 +14591,7 @@ snapshots: '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.27.2': + '@esbuild/linux-riscv64@0.27.3': optional: true '@esbuild/linux-s390x@0.19.12': @@ -15404,7 +14600,7 @@ snapshots: '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.27.2': + '@esbuild/linux-s390x@0.27.3': optional: true '@esbuild/linux-x64@0.19.12': @@ -15413,13 +14609,13 @@ snapshots: '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.2': + '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.27.2': + '@esbuild/netbsd-arm64@0.27.3': optional: true '@esbuild/netbsd-x64@0.19.12': @@ -15428,13 +14624,13 @@ snapshots: '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.27.2': + '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.2': + '@esbuild/openbsd-arm64@0.27.3': optional: true '@esbuild/openbsd-x64@0.19.12': @@ -15443,13 +14639,13 @@ snapshots: '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.27.2': + '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.2': + '@esbuild/openharmony-arm64@0.27.3': optional: true '@esbuild/sunos-x64@0.19.12': @@ -15458,7 +14654,7 @@ snapshots: '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.2': + '@esbuild/sunos-x64@0.27.3': optional: true '@esbuild/win32-arm64@0.19.12': @@ -15467,7 +14663,7 @@ snapshots: '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.2': + '@esbuild/win32-arm64@0.27.3': optional: true '@esbuild/win32-ia32@0.19.12': @@ -15476,7 +14672,7 @@ snapshots: '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.2': + '@esbuild/win32-ia32@0.27.3': optional: true '@esbuild/win32-x64@0.19.12': @@ -15485,7 +14681,7 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.2': + '@esbuild/win32-x64@0.27.3': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': @@ -15540,12 +14736,12 @@ snapshots: dependencies: urlpattern-polyfill: 8.0.2 - '@faker-js/faker@10.2.0': {} + '@faker-js/faker@10.3.0': {} '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.8.0 + prettier: 3.8.1 '@floating-ui/core@1.7.3': dependencies: @@ -15565,10 +14761,10 @@ snapshots: decimal.js: 10.6.0 tslib: 2.8.1 - '@formatjs/ecma402-abstract@3.0.8': + '@formatjs/ecma402-abstract@3.1.1': dependencies: - '@formatjs/fast-memoize': 3.0.3 - '@formatjs/intl-localematcher': 0.7.5 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/intl-localematcher': 0.8.1 decimal.js: 10.6.0 tslib: 2.8.1 @@ -15576,7 +14772,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@formatjs/fast-memoize@3.0.3': + '@formatjs/fast-memoize@3.1.0': dependencies: tslib: 2.8.1 @@ -15586,10 +14782,10 @@ snapshots: '@formatjs/icu-skeleton-parser': 1.8.16 tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@3.3.0': + '@formatjs/icu-messageformat-parser@3.5.1': dependencies: - '@formatjs/ecma402-abstract': 3.0.8 - '@formatjs/icu-skeleton-parser': 2.0.8 + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/icu-skeleton-parser': 2.1.1 tslib: 2.8.1 '@formatjs/icu-skeleton-parser@1.8.16': @@ -15597,18 +14793,18 @@ snapshots: '@formatjs/ecma402-abstract': 2.3.6 tslib: 2.8.1 - '@formatjs/icu-skeleton-parser@2.0.8': + '@formatjs/icu-skeleton-parser@2.1.1': dependencies: - '@formatjs/ecma402-abstract': 3.0.8 + '@formatjs/ecma402-abstract': 3.1.1 tslib: 2.8.1 '@formatjs/intl-localematcher@0.6.2': dependencies: tslib: 2.8.1 - '@formatjs/intl-localematcher@0.7.5': + '@formatjs/intl-localematcher@0.8.1': dependencies: - '@formatjs/fast-memoize': 3.0.3 + '@formatjs/fast-memoize': 3.1.0 tslib: 2.8.1 '@fortawesome/fontawesome-common-types@7.1.0': {} @@ -15621,10 +14817,10 @@ snapshots: dependencies: '@fortawesome/fontawesome-common-types': 7.1.0 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) lodash: 4.17.23 '@grpc/grpc-js@1.14.3': @@ -15769,26 +14965,22 @@ snapshots: '@immich/justified-layout-wasm@0.4.3': {} - '@immich/sdk@file:open-api/typescript-sdk': - dependencies: - '@oazapfts/runtime': 1.1.0 - - '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.48.0)': + '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.50.0)': dependencies: front-matter: 4.0.2 marked: 17.0.1 node-emoji: 2.2.0 - svelte: 5.48.0 + svelte: 5.50.0 - '@immich/ui@0.61.4(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)': + '@immich/ui@0.62.1(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)': dependencies: - '@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.48.0) + '@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.50.0) '@internationalized/date': 3.10.0 '@mdi/js': 7.4.47 - bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) + bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0) luxon: 3.7.2 simple-icons: 16.4.0 - svelte: 5.48.0 + svelte: 5.50.0 svelte-highlight: 7.9.0 tailwind-merge: 3.4.0 tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) @@ -15798,247 +14990,143 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/ansi@2.0.3': {} - - '@inquirer/checkbox@4.3.2(@types/node@24.10.9)': + '@inquirer/checkbox@4.3.2(@types/node@24.10.13)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.13) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/checkbox@5.0.4(@types/node@24.10.9)': + '@inquirer/confirm@5.1.21(@types/node@24.10.13)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) + '@inquirer/type': 3.0.10(@types/node@24.10.13) optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/confirm@5.1.21(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.9) - '@inquirer/type': 3.0.10(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/confirm@6.0.4(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/type': 4.0.3(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/core@10.3.2(@types/node@24.10.9)': + '@inquirer/core@10.3.2(@types/node@24.10.13)': dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.13) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/core@11.1.1(@types/node@24.10.9)': + '@inquirer/editor@4.2.23(@types/node@24.10.13)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.10.9) - cli-width: 4.1.0 - mute-stream: 3.0.0 - signal-exit: 4.1.0 - wrap-ansi: 9.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.13) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.13) + '@inquirer/type': 3.0.10(@types/node@24.10.13) optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/editor@4.2.23(@types/node@24.10.9)': + '@inquirer/expand@4.0.23(@types/node@24.10.13)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.9) - '@inquirer/external-editor': 1.0.3(@types/node@24.10.9) - '@inquirer/type': 3.0.10(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/editor@5.0.4(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/external-editor': 2.0.3(@types/node@24.10.9) - '@inquirer/type': 4.0.3(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/expand@4.0.23(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.9) - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) + '@inquirer/type': 3.0.10(@types/node@24.10.13) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/expand@5.0.4(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/type': 4.0.3(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/external-editor@1.0.3(@types/node@24.10.9)': + '@inquirer/external-editor@1.0.3(@types/node@24.10.13)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/external-editor@2.0.3(@types/node@24.10.9)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@inquirer/figures@1.0.15': {} - '@inquirer/figures@2.0.3': {} - - '@inquirer/input@4.3.1(@types/node@24.10.9)': + '@inquirer/input@4.3.1(@types/node@24.10.13)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.9) - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) + '@inquirer/type': 3.0.10(@types/node@24.10.13) optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/input@5.0.4(@types/node@24.10.9)': + '@inquirer/number@3.0.23(@types/node@24.10.13)': dependencies: - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/type': 4.0.3(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) + '@inquirer/type': 3.0.10(@types/node@24.10.13) optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/number@3.0.23(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.9) - '@inquirer/type': 3.0.10(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/number@4.0.4(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/type': 4.0.3(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/password@4.0.23(@types/node@24.10.9)': + '@inquirer/password@4.0.23(@types/node@24.10.13)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.9) - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) + '@inquirer/type': 3.0.10(@types/node@24.10.13) optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/password@5.0.4(@types/node@24.10.9)': + '@inquirer/prompts@7.10.1(@types/node@24.10.13)': dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/type': 4.0.3(@types/node@24.10.9) + '@inquirer/checkbox': 4.3.2(@types/node@24.10.13) + '@inquirer/confirm': 5.1.21(@types/node@24.10.13) + '@inquirer/editor': 4.2.23(@types/node@24.10.13) + '@inquirer/expand': 4.0.23(@types/node@24.10.13) + '@inquirer/input': 4.3.1(@types/node@24.10.13) + '@inquirer/number': 3.0.23(@types/node@24.10.13) + '@inquirer/password': 4.0.23(@types/node@24.10.13) + '@inquirer/rawlist': 4.1.11(@types/node@24.10.13) + '@inquirer/search': 3.2.2(@types/node@24.10.13) + '@inquirer/select': 4.4.2(@types/node@24.10.13) optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/prompts@7.3.2(@types/node@24.10.9)': + '@inquirer/prompts@7.3.2(@types/node@24.10.13)': dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@24.10.9) - '@inquirer/confirm': 5.1.21(@types/node@24.10.9) - '@inquirer/editor': 4.2.23(@types/node@24.10.9) - '@inquirer/expand': 4.0.23(@types/node@24.10.9) - '@inquirer/input': 4.3.1(@types/node@24.10.9) - '@inquirer/number': 3.0.23(@types/node@24.10.9) - '@inquirer/password': 4.0.23(@types/node@24.10.9) - '@inquirer/rawlist': 4.1.11(@types/node@24.10.9) - '@inquirer/search': 3.2.2(@types/node@24.10.9) - '@inquirer/select': 4.4.2(@types/node@24.10.9) + '@inquirer/checkbox': 4.3.2(@types/node@24.10.13) + '@inquirer/confirm': 5.1.21(@types/node@24.10.13) + '@inquirer/editor': 4.2.23(@types/node@24.10.13) + '@inquirer/expand': 4.0.23(@types/node@24.10.13) + '@inquirer/input': 4.3.1(@types/node@24.10.13) + '@inquirer/number': 3.0.23(@types/node@24.10.13) + '@inquirer/password': 4.0.23(@types/node@24.10.13) + '@inquirer/rawlist': 4.1.11(@types/node@24.10.13) + '@inquirer/search': 3.2.2(@types/node@24.10.13) + '@inquirer/select': 4.4.2(@types/node@24.10.13) optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/prompts@8.2.0(@types/node@24.10.9)': + '@inquirer/rawlist@4.1.11(@types/node@24.10.13)': dependencies: - '@inquirer/checkbox': 5.0.4(@types/node@24.10.9) - '@inquirer/confirm': 6.0.4(@types/node@24.10.9) - '@inquirer/editor': 5.0.4(@types/node@24.10.9) - '@inquirer/expand': 5.0.4(@types/node@24.10.9) - '@inquirer/input': 5.0.4(@types/node@24.10.9) - '@inquirer/number': 4.0.4(@types/node@24.10.9) - '@inquirer/password': 5.0.4(@types/node@24.10.9) - '@inquirer/rawlist': 5.2.0(@types/node@24.10.9) - '@inquirer/search': 4.1.0(@types/node@24.10.9) - '@inquirer/select': 5.0.4(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/rawlist@4.1.11(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.9) - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) + '@inquirer/type': 3.0.10(@types/node@24.10.13) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/rawlist@5.2.0(@types/node@24.10.9)': + '@inquirer/search@3.2.2(@types/node@24.10.13)': dependencies: - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/type': 4.0.3(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/search@3.2.2(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.13) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/search@4.1.0(@types/node@24.10.9)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.10.9) - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/select@4.4.2(@types/node@24.10.9)': + '@inquirer/select@4.4.2(@types/node@24.10.13)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/core': 10.3.2(@types/node@24.10.13) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.13) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 - '@inquirer/select@5.0.4(@types/node@24.10.9)': - dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.1(@types/node@24.10.9) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@24.10.9) + '@inquirer/type@3.0.10(@types/node@24.10.13)': optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/type@3.0.10(@types/node@24.10.9)': - optionalDependencies: - '@types/node': 24.10.9 - - '@inquirer/type@4.0.3(@types/node@24.10.9)': - optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@internationalized/date@3.10.0': dependencies: @@ -16048,7 +15136,7 @@ snapshots: '@isaacs/balanced-match@4.0.1': {} - '@isaacs/brace-expansion@5.0.0': + '@isaacs/brace-expansion@5.0.1': dependencies: '@isaacs/balanced-match': 4.0.1 @@ -16076,7 +15164,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -16169,8 +15257,8 @@ snapshots: '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) browserslist: 4.28.1 transitivePeerDependencies: - eslint @@ -16223,7 +15311,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.7.3 + semver: 7.7.4 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -16239,7 +15327,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.7.3 + semver: 7.7.4 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -16269,6 +15357,8 @@ snapshots: '@mapbox/whoots-js@3.1.0': {} + '@maplibre/geojson-vt@5.0.4': {} + '@maplibre/maplibre-gl-style-spec@24.4.1': dependencies: '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -16279,17 +15369,17 @@ snapshots: rw: 1.3.3 tinyqueue: 3.0.0 - '@maplibre/mlt@1.1.2': + '@maplibre/mlt@1.1.6': dependencies: '@mapbox/point-geometry': 1.1.0 - '@maplibre/vt-pbf@4.2.0': + '@maplibre/vt-pbf@4.2.1': dependencies: '@mapbox/point-geometry': 1.1.0 '@mapbox/vector-tile': 2.0.4 - '@types/geojson-vt': 3.2.5 + '@maplibre/geojson-vt': 5.0.4 + '@types/geojson': 7946.0.16 '@types/supercluster': 7.1.3 - geojson-vt: 4.0.2 pbf: 4.0.1 supercluster: 8.0.1 @@ -16335,10 +15425,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1)': + '@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.2.8 + '@types/react': 19.2.13 react: 18.3.1 '@mermaid-js/parser@0.6.3': @@ -16367,49 +15457,49 @@ snapshots: '@namnode/store@0.1.0': {} - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(bullmq@5.66.5)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(bullmq@5.67.3)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.66.5 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.67.3 tslib: 2.8.1 - '@nestjs/cli@11.0.15(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9)': + '@nestjs/cli@11.0.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@24.10.13)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.19(@types/node@24.10.9)(chokidar@4.0.3) - '@inquirer/prompts': 8.2.0(@types/node@24.10.9) + '@angular-devkit/schematics-cli': 19.2.19(@types/node@24.10.13)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@24.10.13) '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))) glob: 13.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.8(@swc/helpers@0.5.17) + '@swc/core': 1.15.11(@swc/helpers@0.5.17) transitivePeerDependencies: - '@types/node' - esbuild - uglify-js - webpack-cli - '@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.3.0 iterare: 1.2.1 @@ -16424,9 +15514,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -16436,22 +15526,22 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) - '@nestjs/websockets': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + '@nestjs/websockets': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/platform-express@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': + '@nestjs/platform-express@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cors: 2.8.5 + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 express: 5.2.1 multer: 2.0.2 path-to-regexp: 8.3.0 @@ -16459,10 +15549,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.13)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3 tslib: 2.8.1 @@ -16471,11 +15561,11 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': + '@nestjs/schedule@6.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cron: 4.3.5 + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cron: 4.4.0 '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': dependencies: @@ -16488,14 +15578,14 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.6(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 path-to-regexp: 8.3.0 reflect-metadata: 0.2.2 swagger-ui-dist: 5.31.0 @@ -16503,25 +15593,25 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/testing@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12)': + '@nestjs/testing@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + '@nestjs/platform-express': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) - '@nestjs/websockets@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-socket.io@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.13)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -16542,14 +15632,14 @@ snapshots: agent-base: 7.1.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - lru-cache: 11.2.4 + lru-cache: 11.2.6 socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color '@npmcli/fs@5.0.0': dependencies: - semver: 7.7.3 + semver: 7.7.4 '@nuxt/opencollective@0.4.1': dependencies: @@ -16557,291 +15647,291 @@ snapshots: '@oazapfts/runtime@1.1.0': {} - '@opentelemetry/api-logs@0.210.0': + '@opentelemetry/api-logs@0.211.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/configuration@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/configuration@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) yaml: 2.8.2 - '@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.210.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.210.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/host-metrics@0.36.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-http@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.58.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.59.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.56.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.57.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.62.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.63.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) '@types/pg': 8.15.6 '@types/pg-pool': 2.0.7 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.210.0 + '@opentelemetry/api-logs': 0.211.0 import-in-the-middle: 2.0.0 require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.211.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.210.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) protobufjs: 8.0.0 - '@opentelemetry/propagator-b3@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-b3@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common@0.38.2': {} - '@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/sdk-logs@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.210.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.210.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.210.0 - '@opentelemetry/configuration': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/context-async-hooks': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/api-logs': 0.211.0 + '@opentelemetry/configuration': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/sdk-trace-node@2.4.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.5.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions@1.38.0': {} + '@opentelemetry/semantic-conventions@1.39.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) '@paralleldrive/cuid2@2.3.1': dependencies: @@ -16943,9 +16033,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.57.0': + '@playwright/test@1.58.2': dependencies: - playwright: 1.57.0 + playwright: 1.58.2 '@pnpm/config.env-replace@1.1.0': {} @@ -16984,117 +16074,117 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@react-email/body@0.1.0(react@19.2.3)': + '@react-email/body@0.1.0(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/button@0.2.0(react@19.2.3)': + '@react-email/button@0.2.0(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/code-block@0.1.0(react@19.2.3)': + '@react-email/code-block@0.1.0(react@19.2.4)': dependencies: prismjs: 1.30.0 - react: 19.2.3 + react: 19.2.4 - '@react-email/code-inline@0.0.5(react@19.2.3)': + '@react-email/code-inline@0.0.5(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/column@0.0.13(react@19.2.3)': + '@react-email/column@0.0.13(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/components@0.5.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@react-email/components@0.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@react-email/body': 0.1.0(react@19.2.3) - '@react-email/button': 0.2.0(react@19.2.3) - '@react-email/code-block': 0.1.0(react@19.2.3) - '@react-email/code-inline': 0.0.5(react@19.2.3) - '@react-email/column': 0.0.13(react@19.2.3) - '@react-email/container': 0.0.15(react@19.2.3) - '@react-email/font': 0.0.9(react@19.2.3) - '@react-email/head': 0.0.12(react@19.2.3) - '@react-email/heading': 0.0.15(react@19.2.3) - '@react-email/hr': 0.0.11(react@19.2.3) - '@react-email/html': 0.0.11(react@19.2.3) - '@react-email/img': 0.0.11(react@19.2.3) - '@react-email/link': 0.0.12(react@19.2.3) - '@react-email/markdown': 0.0.16(react@19.2.3) - '@react-email/preview': 0.0.13(react@19.2.3) - '@react-email/render': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@react-email/row': 0.0.12(react@19.2.3) - '@react-email/section': 0.0.16(react@19.2.3) - '@react-email/tailwind': 1.2.2(react@19.2.3) - '@react-email/text': 0.1.5(react@19.2.3) - react: 19.2.3 + '@react-email/body': 0.1.0(react@19.2.4) + '@react-email/button': 0.2.0(react@19.2.4) + '@react-email/code-block': 0.1.0(react@19.2.4) + '@react-email/code-inline': 0.0.5(react@19.2.4) + '@react-email/column': 0.0.13(react@19.2.4) + '@react-email/container': 0.0.15(react@19.2.4) + '@react-email/font': 0.0.9(react@19.2.4) + '@react-email/head': 0.0.12(react@19.2.4) + '@react-email/heading': 0.0.15(react@19.2.4) + '@react-email/hr': 0.0.11(react@19.2.4) + '@react-email/html': 0.0.11(react@19.2.4) + '@react-email/img': 0.0.11(react@19.2.4) + '@react-email/link': 0.0.12(react@19.2.4) + '@react-email/markdown': 0.0.16(react@19.2.4) + '@react-email/preview': 0.0.13(react@19.2.4) + '@react-email/render': 1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-email/row': 0.0.12(react@19.2.4) + '@react-email/section': 0.0.16(react@19.2.4) + '@react-email/tailwind': 1.2.2(react@19.2.4) + '@react-email/text': 0.1.5(react@19.2.4) + react: 19.2.4 transitivePeerDependencies: - react-dom - '@react-email/container@0.0.15(react@19.2.3)': + '@react-email/container@0.0.15(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/font@0.0.9(react@19.2.3)': + '@react-email/font@0.0.9(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/head@0.0.12(react@19.2.3)': + '@react-email/head@0.0.12(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/heading@0.0.15(react@19.2.3)': + '@react-email/heading@0.0.15(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/hr@0.0.11(react@19.2.3)': + '@react-email/hr@0.0.11(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/html@0.0.11(react@19.2.3)': + '@react-email/html@0.0.11(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/img@0.0.11(react@19.2.3)': + '@react-email/img@0.0.11(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/link@0.0.12(react@19.2.3)': + '@react-email/link@0.0.12(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/markdown@0.0.16(react@19.2.3)': + '@react-email/markdown@0.0.16(react@19.2.4)': dependencies: marked: 15.0.12 - react: 19.2.3 + react: 19.2.4 - '@react-email/preview@0.0.13(react@19.2.3)': + '@react-email/preview@0.0.13(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/render@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@react-email/render@1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: html-to-text: 9.0.5 - prettier: 3.8.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + prettier: 3.8.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) react-promise-suspense: 0.3.4 - '@react-email/row@0.0.12(react@19.2.3)': + '@react-email/row@0.0.12(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/section@0.0.16(react@19.2.3)': + '@react-email/section@0.0.16(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/tailwind@1.2.2(react@19.2.3)': + '@react-email/tailwind@1.2.2(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/text@0.1.5(react@19.2.3)': + '@react-email/text@0.1.5(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.8)': dependencies: @@ -17222,280 +16312,6 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 - '@smithy/abort-controller@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/config-resolver@4.4.6': - dependencies: - '@smithy/node-config-provider': 4.3.8 - '@smithy/types': 4.12.0 - '@smithy/util-config-provider': 4.2.0 - '@smithy/util-endpoints': 3.2.8 - '@smithy/util-middleware': 4.2.8 - tslib: 2.8.1 - - '@smithy/core@3.20.7': - dependencies: - '@smithy/middleware-serde': 4.2.9 - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-stream': 4.5.10 - '@smithy/util-utf8': 4.2.0 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.2.8': - dependencies: - '@smithy/node-config-provider': 4.3.8 - '@smithy/property-provider': 4.2.8 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.3.9': - dependencies: - '@smithy/protocol-http': 5.3.8 - '@smithy/querystring-builder': 4.2.8 - '@smithy/types': 4.12.0 - '@smithy/util-base64': 4.3.0 - tslib: 2.8.1 - - '@smithy/hash-node@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/is-array-buffer@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.2.8': - dependencies: - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@4.4.8': - dependencies: - '@smithy/core': 3.20.7 - '@smithy/middleware-serde': 4.2.9 - '@smithy/node-config-provider': 4.3.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - '@smithy/url-parser': 4.2.8 - '@smithy/util-middleware': 4.2.8 - tslib: 2.8.1 - - '@smithy/middleware-retry@4.4.24': - dependencies: - '@smithy/node-config-provider': 4.3.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/service-error-classification': 4.2.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-retry': 4.2.8 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 - - '@smithy/middleware-serde@4.2.9': - dependencies: - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/middleware-stack@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/node-config-provider@4.3.8': - dependencies: - '@smithy/property-provider': 4.2.8 - '@smithy/shared-ini-file-loader': 4.4.3 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.4.8': - dependencies: - '@smithy/abort-controller': 4.2.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/querystring-builder': 4.2.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/property-provider@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/protocol-http@5.3.8': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/querystring-builder@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - '@smithy/util-uri-escape': 4.2.0 - tslib: 2.8.1 - - '@smithy/querystring-parser@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/service-error-classification@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - - '@smithy/shared-ini-file-loader@4.4.3': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/signature-v4@5.3.8': - dependencies: - '@smithy/is-array-buffer': 4.2.0 - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - '@smithy/util-hex-encoding': 4.2.0 - '@smithy/util-middleware': 4.2.8 - '@smithy/util-uri-escape': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/smithy-client@4.10.9': - dependencies: - '@smithy/core': 3.20.7 - '@smithy/middleware-endpoint': 4.4.8 - '@smithy/middleware-stack': 4.2.8 - '@smithy/protocol-http': 5.3.8 - '@smithy/types': 4.12.0 - '@smithy/util-stream': 4.5.10 - tslib: 2.8.1 - - '@smithy/types@4.12.0': - dependencies: - tslib: 2.8.1 - - '@smithy/url-parser@4.2.8': - dependencies: - '@smithy/querystring-parser': 4.2.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/util-base64@4.3.0': - dependencies: - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/util-body-length-browser@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-body-length-node@4.2.1': - dependencies: - tslib: 2.8.1 - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-buffer-from@4.2.0': - dependencies: - '@smithy/is-array-buffer': 4.2.0 - tslib: 2.8.1 - - '@smithy/util-config-provider@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-defaults-mode-browser@4.3.23': - dependencies: - '@smithy/property-provider': 4.2.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/util-defaults-mode-node@4.2.26': - dependencies: - '@smithy/config-resolver': 4.4.6 - '@smithy/credential-provider-imds': 4.2.8 - '@smithy/node-config-provider': 4.3.8 - '@smithy/property-provider': 4.2.8 - '@smithy/smithy-client': 4.10.9 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/util-endpoints@3.2.8': - dependencies: - '@smithy/node-config-provider': 4.3.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/util-hex-encoding@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-middleware@4.2.8': - dependencies: - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/util-retry@4.2.8': - dependencies: - '@smithy/service-error-classification': 4.2.8 - '@smithy/types': 4.12.0 - tslib: 2.8.1 - - '@smithy/util-stream@4.5.10': - dependencies: - '@smithy/fetch-http-handler': 5.3.9 - '@smithy/node-http-handler': 4.4.8 - '@smithy/types': 4.12.0 - '@smithy/util-base64': 4.3.0 - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-hex-encoding': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/util-uri-escape@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@4.2.0': - dependencies: - '@smithy/util-buffer-from': 4.2.0 - tslib: 2.8.1 - - '@smithy/uuid@1.1.0': - dependencies: - tslib: 2.8.1 - '@socket.io/component-emitter@3.1.2': {} '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.6)': @@ -17511,33 +16327,33 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.9(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/enhanced-img@0.10.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) magic-string: 0.30.21 sharp: 0.34.5 - svelte: 5.48.0 - svelte-parse-markup: 0.1.5(svelte@5.48.0) - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.50.0 + svelte-parse-markup: 0.1.5(svelte@5.50.0) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-imagetools: 9.0.2(rollup@4.55.1) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -17547,32 +16363,32 @@ snapshots: magic-string: 0.30.21 mrmime: 2.0.1 sade: 1.8.1 - set-cookie-parser: 2.7.2 + set-cookie-parser: 3.0.1 sirv: 3.0.2 - svelte: 5.48.0 - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.50.0 + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@opentelemetry/api': 1.9.0 typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) debug: 4.4.3 - svelte: 5.48.0 - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.50.0 + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.48.0 - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + svelte: 5.50.0 + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -17669,51 +16485,51 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.8': + '@swc/core-darwin-arm64@1.15.11': optional: true - '@swc/core-darwin-x64@1.15.8': + '@swc/core-darwin-x64@1.15.11': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.8': + '@swc/core-linux-arm-gnueabihf@1.15.11': optional: true - '@swc/core-linux-arm64-gnu@1.15.8': + '@swc/core-linux-arm64-gnu@1.15.11': optional: true - '@swc/core-linux-arm64-musl@1.15.8': + '@swc/core-linux-arm64-musl@1.15.11': optional: true - '@swc/core-linux-x64-gnu@1.15.8': + '@swc/core-linux-x64-gnu@1.15.11': optional: true - '@swc/core-linux-x64-musl@1.15.8': + '@swc/core-linux-x64-musl@1.15.11': optional: true - '@swc/core-win32-arm64-msvc@1.15.8': + '@swc/core-win32-arm64-msvc@1.15.11': optional: true - '@swc/core-win32-ia32-msvc@1.15.8': + '@swc/core-win32-ia32-msvc@1.15.11': optional: true - '@swc/core-win32-x64-msvc@1.15.8': + '@swc/core-win32-x64-msvc@1.15.11': optional: true - '@swc/core@1.15.8(@swc/helpers@0.5.17)': + '@swc/core@1.15.11(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.8 - '@swc/core-darwin-x64': 1.15.8 - '@swc/core-linux-arm-gnueabihf': 1.15.8 - '@swc/core-linux-arm64-gnu': 1.15.8 - '@swc/core-linux-arm64-musl': 1.15.8 - '@swc/core-linux-x64-gnu': 1.15.8 - '@swc/core-linux-x64-musl': 1.15.8 - '@swc/core-win32-arm64-msvc': 1.15.8 - '@swc/core-win32-ia32-msvc': 1.15.8 - '@swc/core-win32-x64-msvc': 1.15.8 + '@swc/core-darwin-arm64': 1.15.11 + '@swc/core-darwin-x64': 1.15.11 + '@swc/core-linux-arm-gnueabihf': 1.15.11 + '@swc/core-linux-arm64-gnu': 1.15.11 + '@swc/core-linux-arm64-musl': 1.15.11 + '@swc/core-linux-x64-gnu': 1.15.11 + '@swc/core-linux-x64-musl': 1.15.11 + '@swc/core-win32-arm64-msvc': 1.15.11 + '@swc/core-win32-ia32-msvc': 1.15.11 + '@swc/core-win32-x64-msvc': 1.15.11 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -17733,7 +16549,7 @@ snapshots: '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 jiti: 2.6.1 lightningcss: 1.30.2 magic-string: 0.30.21 @@ -17791,16 +16607,16 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -17818,18 +16634,18 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte-core@1.0.0(svelte@5.48.0)': + '@testing-library/svelte-core@1.0.0(svelte@5.50.0)': dependencies: - svelte: 5.48.0 + svelte: 5.50.0 - '@testing-library/svelte@5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@testing-library/svelte@5.3.1(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.0.0(svelte@5.48.0) - svelte: 5.48.0 + '@testing-library/svelte-core': 1.0.0(svelte@5.50.0) + svelte: 5.50.0 optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -17870,7 +16686,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/archiver@7.0.0': dependencies: @@ -17882,16 +16698,16 @@ snapshots: '@types/bcrypt@6.0.0': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/bonjour@3.5.13': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/braces@3.0.5': {} @@ -17913,21 +16729,21 @@ snapshots: '@types/cli-progress@3.11.6': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/compression@1.8.1': dependencies: '@types/express': 5.0.6 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.1.0 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/content-disposition@0.5.9': {} @@ -17944,11 +16760,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.6 '@types/keygrip': 1.0.6 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/cors@2.8.19': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/d3-array@3.2.2': {} @@ -18075,13 +16891,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/ssh2': 1.15.5 '@types/dockerode@3.3.47': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/ssh2': 1.15.5 '@types/dom-to-image@2.6.7': {} @@ -18104,14 +16920,14 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -18137,11 +16953,7 @@ snapshots: '@types/fluent-ffmpeg@2.1.28': dependencies: - '@types/node': 24.10.9 - - '@types/geojson-vt@3.2.5': - dependencies: - '@types/geojson': 7946.0.16 + '@types/node': 24.10.13 '@types/geojson@7946.0.16': {} @@ -18169,7 +16981,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/inquirer@8.2.12': dependencies: @@ -18193,7 +17005,7 @@ snapshots: '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/justified-layout@4.1.4': {} @@ -18212,7 +17024,7 @@ snapshots: '@types/http-errors': 2.0.5 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.9 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/leaflet@1.9.21': dependencies: @@ -18242,7 +17054,7 @@ snapshots: '@types/mock-fs@4.13.4': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/ms@2.1.0': {} @@ -18252,7 +17064,7 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/node@17.0.45': {} @@ -18260,31 +17072,24 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.30': - dependencies: - undici-types: 6.21.0 - - '@types/node@24.10.9': + '@types/node@24.10.13': dependencies: undici-types: 7.16.0 - '@types/node@25.0.9': + '@types/node@25.2.3': dependencies: undici-types: 7.16.0 optional: true - '@types/nodemailer@7.0.5': + '@types/nodemailer@7.0.9': dependencies: - '@aws-sdk/client-sesv2': 3.971.0 - '@types/node': 24.10.9 - transitivePeerDependencies: - - aws-crt + '@types/node': 24.10.13 '@types/oidc-provider@9.5.0': dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.1 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/parse5@5.0.3': {} @@ -18294,13 +17099,13 @@ snapshots: '@types/pg@8.15.6': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/pg@8.16.0': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 pg-protocol: 1.11.0 pg-types: 2.2.0 @@ -18308,13 +17113,13 @@ snapshots: '@types/pngjs@6.0.5': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/prismjs@1.26.5': {} '@types/qrcode@1.5.6': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/qs@6.14.0': {} @@ -18323,27 +17128,27 @@ snapshots: '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.8 + '@types/react': 19.2.13 '@types/react-router': 5.1.20 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.8 + '@types/react': 19.2.13 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.8 + '@types/react': 19.2.13 - '@types/react@19.2.8': + '@types/react@19.2.13': dependencies: csstype: 3.2.3 '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/retry@0.12.2': {} @@ -18353,18 +17158,18 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/semver@7.7.1': {} '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/send@1.2.1': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/serve-index@1.9.4': dependencies: @@ -18373,25 +17178,25 @@ snapshots: '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/send': 0.17.6 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/sockjs@0.3.36': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/ssh2-streams@0.1.13': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/ssh2@0.5.52': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/ssh2-streams': 0.1.13 '@types/ssh2@1.15.5': @@ -18402,7 +17207,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 24.10.9 + '@types/node': 24.10.13 form-data: 4.0.5 '@types/supercluster@7.1.3': @@ -18416,7 +17221,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/trusted-types@2.0.7': optional: true @@ -18433,7 +17238,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 '@types/yargs-parser@21.0.3': {} @@ -18441,14 +17246,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -18457,41 +17262,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) - '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.53.0': + '@typescript-eslint/scope-manager@8.54.0': dependencies: - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 - '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) @@ -18499,44 +17304,44 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.53.0': {} + '@typescript-eslint/types@8.54.0': {} - '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/visitor-keys': 8.53.0 + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 minimatch: 9.0.5 - semver: 7.7.3 + semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.53.0 - '@typescript-eslint/types': 8.53.0 - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.54.0 + '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.53.0': + '@typescript-eslint/visitor-keys@8.54.0': dependencies: - '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/types': 8.54.0 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} '@vercel/oidc@3.0.5': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -18551,11 +17356,11 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -18570,7 +17375,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -18582,21 +17387,21 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -18704,14 +17509,14 @@ snapshots: '@xtuc/long@4.2.2': {} - '@zoom-image/core@0.41.4': + '@zoom-image/core@0.42.0': dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.8(svelte@5.48.0)': + '@zoom-image/svelte@0.3.9(svelte@5.50.0)': dependencies: - '@zoom-image/core': 0.41.4 - svelte: 5.48.0 + '@zoom-image/core': 0.42.0 + svelte: 5.50.0 abab@2.0.6: optional: true @@ -18953,10 +17758,10 @@ snapshots: dependencies: immediate: 3.3.0 - autoprefixer@10.4.23(postcss@8.5.6): + autoprefixer@10.4.24(postcss@8.5.6): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 + caniuse-lite: 1.0.30001769 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 @@ -19048,7 +17853,7 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.9.7: {} + baseline-browser-mapping@2.9.19: {} batch-cluster@16.0.0: {} @@ -19063,7 +17868,7 @@ snapshots: bcrypt@6.0.0: dependencies: node-addon-api: 8.5.0 - node-gyp: 12.1.0 + node-gyp: 12.2.0 node-gyp-build: 4.8.4 transitivePeerDependencies: - supports-color @@ -19072,15 +17877,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) - svelte: 5.48.0 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) + runed: 0.35.1(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0) + svelte: 5.50.0 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0) tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -19129,8 +17934,6 @@ snapshots: boolbase@1.0.0: {} - bowser@2.13.1: {} - boxen@6.2.1: dependencies: ansi-align: 3.0.1 @@ -19168,11 +17971,11 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.7 - caniuse-lite: 1.0.30001760 - electron-to-chromium: 1.5.267 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 node-releases: 2.0.27 - update-browserslist-db: 1.2.2(browserslist@4.28.1) + update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-crc32@1.0.0: {} @@ -19195,10 +17998,10 @@ snapshots: builtin-modules@5.0.0: {} - bullmq@5.66.5: + bullmq@5.67.3: dependencies: cron-parser: 4.9.0 - ioredis: 5.9.1 + ioredis: 5.9.2 msgpackr: 1.11.5 node-abort-controller: 3.1.1 semver: 7.7.3 @@ -19229,14 +18032,14 @@ snapshots: dependencies: '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 - glob: 13.0.0 - lru-cache: 11.2.4 + glob: 13.0.2 + lru-cache: 11.2.6 minipass: 7.1.2 minipass-collect: 2.0.1 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 p-map: 7.0.4 - ssri: 13.0.0 + ssri: 13.0.1 unique-filename: 5.0.0 cacheable-lookup@7.0.0: {} @@ -19286,11 +18089,11 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 + caniuse-lite: 1.0.30001769 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001760: {} + caniuse-lite@1.0.30001769: {} canvas@2.11.2: dependencies: @@ -19314,9 +18117,9 @@ snapshots: ccount@2.0.1: {} - ce-la-react@0.3.2(react@19.2.3): + ce-la-react@0.3.2(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 chai@5.3.3: dependencies: @@ -19662,7 +18465,7 @@ snapshots: core-util-is@1.0.3: {} - cors@2.8.5: + cors@2.8.6: dependencies: object-assign: 4.1.1 vary: 1.1.2 @@ -19703,7 +18506,7 @@ snapshots: dependencies: luxon: 3.7.2 - cron@4.3.5: + cron@4.4.0: dependencies: '@types/luxon': 3.7.1 luxon: 3.7.2 @@ -19743,7 +18546,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.6) postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: webpack: 5.104.1 @@ -19803,7 +18606,7 @@ snapshots: cssnano-preset-advanced@6.1.2(postcss@8.5.6): dependencies: - autoprefixer: 10.4.23(postcss@8.5.6) + autoprefixer: 10.4.24(postcss@8.5.6) browserslist: 4.28.1 cssnano-preset-default: 6.1.2(postcss@8.5.6) postcss: 8.5.6 @@ -20247,9 +19050,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.8)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.13)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -20329,7 +19132,7 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv@17.2.3: {} + dotenv@17.2.4: {} dunder-proto@1.0.1: dependencies: @@ -20351,7 +19154,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.267: {} + electron-to-chromium@1.5.286: {} emoji-regex@10.6.0: {} @@ -20393,11 +19196,11 @@ snapshots: engine.io@6.6.5: dependencies: '@types/cors': 2.8.19 - '@types/node': 24.10.9 + '@types/node': 24.10.13 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 - cors: 2.8.5 + cors: 2.8.6 debug: 4.4.3 engine.io-parser: 5.2.3 ws: 8.18.3 @@ -20406,7 +19209,7 @@ snapshots: - supports-color - utf-8-validate - enhanced-resolve@5.18.4: + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -20538,34 +19341,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - esbuild@0.27.2: + esbuild@0.27.3: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 escalade@3.2.0: {} @@ -20592,29 +19395,29 @@ snapshots: dependencies: eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-compat@6.0.2(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-compat@6.1.0(eslint@9.39.2(jiti@2.6.1)): dependencies: - '@mdn/browser-compat-data': 5.7.6 + '@mdn/browser-compat-data': 6.1.5 ast-metadata-inferer: 0.8.1 browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 + caniuse-lite: 1.0.30001769 eslint: 9.39.2(jiti@2.6.1) find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 - semver: 7.7.3 + semver: 7.7.4 - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1): dependencies: eslint: 9.39.2(jiti@2.6.1) - prettier: 3.8.0 + prettier: 3.8.1 prettier-linter-helpers: 1.0.1 synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-svelte@3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.48.0): + eslint-plugin-svelte@3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.50.0): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -20625,10 +19428,10 @@ snapshots: postcss: 8.5.6 postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) - semver: 7.7.3 - svelte-eslint-parser: 1.4.1(svelte@5.48.0) + semver: 7.7.4 + svelte-eslint-parser: 1.4.1(svelte@5.50.0) optionalDependencies: - svelte: 5.48.0 + svelte: 5.50.0 transitivePeerDependencies: - ts-node @@ -20651,7 +19454,7 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.13.0 - semver: 7.7.3 + semver: 7.7.4 strip-indent: 4.1.1 eslint-scope@5.1.1: @@ -20730,7 +19533,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.1: + esrap@2.2.3: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -20791,7 +19594,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 require-like: 0.1.2 event-emitter@0.3.5: @@ -20926,10 +19729,10 @@ snapshots: extend@3.0.2: {} - fabric@6.9.1(encoding@0.1.13): + fabric@6.9.1: optionalDependencies: - canvas: 2.11.2(encoding@0.1.13) - jsdom: 20.0.3(canvas@2.11.2(encoding@0.1.13)) + canvas: 2.11.2 + jsdom: 20.0.3(canvas@2.11.2) transitivePeerDependencies: - bufferutil - encoding @@ -20965,10 +19768,6 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-parser@5.2.5: - dependencies: - strnum: 2.1.2 - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -21088,9 +19887,9 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))): dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 chalk: 4.1.2 chokidar: 4.0.3 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -21100,10 +19899,10 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.3 + semver: 7.7.4 tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)) form-data-encoder@2.1.4: {} @@ -21202,8 +20001,6 @@ snapshots: geojson-vt@3.2.1: {} - geojson-vt@4.0.2: {} - geojson@0.5.0: {} get-caller-file@2.0.5: {} @@ -21269,14 +20066,20 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 - minimatch: 10.1.1 + minimatch: 10.1.2 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.1 glob@13.0.0: dependencies: - minimatch: 10.1.1 + minimatch: 10.1.2 + minipass: 7.1.2 + path-scurry: 2.0.1 + + glob@13.0.2: + dependencies: + minimatch: 10.1.2 minipass: 7.1.2 path-scurry: 2.0.1 @@ -21366,11 +20169,12 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.3.0: + happy-dom@20.5.0: dependencies: - '@types/node': 20.19.30 + '@types/node': 24.10.13 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 + entities: 4.5.0 whatwg-mimetype: 3.0.0 ws: 8.19.0 transitivePeerDependencies: @@ -21804,9 +20608,9 @@ snapshots: inline-style-parser@0.2.7: {} - inquirer@8.2.7(@types/node@24.10.9): + inquirer@8.2.7(@types/node@24.10.13): dependencies: - '@inquirer/external-editor': 1.0.3(@types/node@24.10.9) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.13) ansi-escapes: 4.3.2 chalk: 4.1.2 cli-cursor: 3.1.0 @@ -21835,18 +20639,18 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.4 tslib: 2.8.1 - intl-messageformat@11.0.9: + intl-messageformat@11.1.2: dependencies: - '@formatjs/ecma402-abstract': 3.0.8 - '@formatjs/fast-memoize': 3.0.3 - '@formatjs/icu-messageformat-parser': 3.3.0 + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/icu-messageformat-parser': 3.5.1 tslib: 2.8.1 invariant@2.2.4: dependencies: loose-envify: 1.4.0 - ioredis@5.9.1: + ioredis@5.9.2: dependencies: '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 @@ -21989,7 +20793,7 @@ snapshots: isexe@2.0.0: {} - isexe@3.1.1: {} + isexe@4.0.0: {} isobject@3.0.1: {} @@ -22029,7 +20833,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.10.9 + '@types/node': 24.10.13 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -22037,13 +20841,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -22081,7 +20885,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)): + jsdom@20.0.3(canvas@2.11.2): dependencies: abab: 2.0.6 acorn: 8.15.0 @@ -22110,7 +20914,7 @@ snapshots: ws: 8.19.0 xml-name-validator: 4.0.0 optionalDependencies: - canvas: 2.11.2(encoding@0.1.13) + canvas: 2.11.2 transitivePeerDependencies: - bufferutil - supports-color @@ -22226,7 +21030,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.3 + semver: 7.7.4 just-compare@2.3.0: {} @@ -22449,8 +21253,6 @@ snapshots: lodash.uniq@4.5.0: {} - lodash@4.17.21: {} - lodash@4.17.23: {} log-symbols@4.1.0: @@ -22486,7 +21288,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.4: {} + lru-cache@11.2.6: {} lru-cache@5.1.1: dependencies: @@ -22524,7 +21326,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 make-fetch-happen@15.0.3: dependencies: @@ -22532,13 +21334,13 @@ snapshots: cacache: 20.0.3 http-cache-semantics: 4.2.0 minipass: 7.1.2 - minipass-fetch: 5.0.0 + minipass-fetch: 5.0.1 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 negotiator: 1.0.0 proc-log: 6.1.0 promise-retry: 2.0.1 - ssri: 13.0.0 + ssri: 13.0.1 transitivePeerDependencies: - supports-color @@ -22567,7 +21369,7 @@ snapshots: tinyqueue: 2.0.3 vt-pbf: 3.1.3 - maplibre-gl@5.16.0: + maplibre-gl@5.17.0: dependencies: '@mapbox/geojson-rewind': 0.5.2 '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -22576,14 +21378,13 @@ snapshots: '@mapbox/unitbezier': 0.0.1 '@mapbox/vector-tile': 2.0.4 '@mapbox/whoots-js': 3.1.0 + '@maplibre/geojson-vt': 5.0.4 '@maplibre/maplibre-gl-style-spec': 24.4.1 - '@maplibre/mlt': 1.1.2 - '@maplibre/vt-pbf': 4.2.0 + '@maplibre/mlt': 1.1.6 + '@maplibre/vt-pbf': 4.2.1 '@types/geojson': 7946.0.16 - '@types/geojson-vt': 3.2.5 '@types/supercluster': 7.1.3 earcut: 3.0.2 - geojson-vt: 4.0.2 gl-matrix: 3.4.4 kdbush: 4.0.2 murmurhash-js: 1.0.0 @@ -22803,9 +21604,9 @@ snapshots: mdn-data@2.0.30: {} - media-chrome@4.17.2(react@19.2.3): + media-chrome@4.17.2(react@19.2.4): dependencies: - ce-la-react: 0.3.2(react@19.2.3) + ce-la-react: 0.3.2(react@19.2.4) transitivePeerDependencies: - react @@ -23215,9 +22016,9 @@ snapshots: minimalistic-assert@1.0.1: {} - minimatch@10.1.1: + minimatch@10.1.2: dependencies: - '@isaacs/brace-expansion': 5.0.0 + '@isaacs/brace-expansion': 5.0.1 minimatch@3.1.2: dependencies: @@ -23237,10 +22038,10 @@ snapshots: dependencies: minipass: 7.1.2 - minipass-fetch@5.0.0: + minipass-fetch@5.0.1: dependencies: minipass: 7.1.2 - minipass-sized: 1.0.3 + minipass-sized: 2.0.0 minizlib: 3.1.0 optionalDependencies: encoding: 0.1.13 @@ -23253,9 +22054,9 @@ snapshots: dependencies: minipass: 3.3.6 - minipass-sized@1.0.3: + minipass-sized@2.0.0: dependencies: - minipass: 3.3.6 + minipass: 7.1.2 minipass@3.3.6: dependencies: @@ -23346,8 +22147,6 @@ snapshots: mute-stream@2.0.0: {} - mute-stream@3.0.0: {} - mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -23380,39 +22179,39 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@types/inquirer@8.2.12)(@types/node@24.10.9)(typescript@5.9.3): + nest-commander@3.20.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@types/inquirer@8.2.12)(@types/node@24.10.13)(typescript@5.9.3): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/inquirer': 8.2.12 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.3) - inquirer: 8.2.7(@types/node@24.10.9) + inquirer: 8.2.7(@types/node@24.10.13) transitivePeerDependencies: - '@types/node' - typescript - nestjs-cls@5.4.3(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@5.4.3(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(kysely@0.28.2)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(kysely@0.28.2)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.2 reflect-metadata: 0.2.2 tslib: 2.8.1 - nestjs-otel@7.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12): + nestjs-otel@7.0.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13): dependencies: - '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.13)(@nestjs/websockets@11.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': 1.9.0 '@opentelemetry/host-metrics': 0.36.2(@opentelemetry/api@1.9.0) response-time: 2.3.4 @@ -23465,7 +22264,7 @@ snapshots: node-gyp-build@4.8.4: {} - node-gyp@12.1.0: + node-gyp@12.2.0: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.3 @@ -23473,16 +22272,16 @@ snapshots: make-fetch-happen: 15.0.3 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.7.3 - tar: 7.5.2 + semver: 7.7.4 + tar: 7.5.7 tinyglobby: 0.2.15 - which: 6.0.0 + which: 6.0.1 transitivePeerDependencies: - supports-color node-releases@2.0.27: {} - nodemailer@7.0.12: {} + nodemailer@7.0.13: {} nopt@1.0.10: dependencies: @@ -23707,7 +22506,7 @@ snapshots: got: 12.6.1 registry-auth-token: 5.1.0 registry-url: 6.0.1 - semver: 7.7.3 + semver: 7.7.4 package-manager-detector@1.6.0: {} @@ -23732,7 +22531,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.28.6 + '@babel/code-frame': 7.29.0 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -23785,7 +22584,7 @@ snapshots: path-scurry@2.0.1: dependencies: - lru-cache: 11.2.4 + lru-cache: 11.2.6 minipass: 7.1.2 path-source@0.1.3: @@ -23823,13 +22622,13 @@ snapshots: pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.10.0: {} + pg-connection-string@2.11.0: {} pg-int8@1.0.1: {} - pg-pool@3.11.0(pg@8.17.1): + pg-pool@3.11.0(pg@8.18.0): dependencies: - pg: 8.17.1 + pg: 8.18.0 pg-protocol@1.11.0: {} @@ -23841,10 +22640,10 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.17.1: + pg@8.18.0: dependencies: - pg-connection-string: 2.10.0 - pg-pool: 3.11.0(pg@8.17.1) + pg-connection-string: 2.11.0 + pg-pool: 3.11.0(pg@8.18.0) pg-protocol: 1.11.0 pg-types: 2.2.0 pgpass: 1.0.5 @@ -23883,11 +22682,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.57.0: {} + playwright-core@1.58.2: {} - playwright@1.57.0: + playwright@1.58.2: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 @@ -23898,7 +22697,7 @@ snapshots: '@types/leaflet': 1.9.21 fflate: 0.8.2 - pmtiles@4.3.2: + pmtiles@4.4.0: dependencies: fflate: 0.8.2 @@ -24092,7 +22891,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@5.9.3) jiti: 1.21.7 postcss: 8.5.6 - semver: 7.7.3 + semver: 7.7.4 webpack: 5.104.1 transitivePeerDependencies: - typescript @@ -24288,7 +23087,7 @@ snapshots: '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.6) '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6) '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6) - autoprefixer: 10.4.23(postcss@8.5.6) + autoprefixer: 10.4.24(postcss@8.5.6) browserslist: 4.28.1 css-blank-pseudo: 7.0.1(postcss@8.5.6) css-has-pseudo: 7.0.3(postcss@8.5.6) @@ -24419,21 +23218,21 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(prettier@3.8.0)(typescript@5.9.3): + prettier-plugin-organize-imports@4.3.0(prettier@3.8.1)(typescript@5.9.3): dependencies: - prettier: 3.8.0 + prettier: 3.8.1 typescript: 5.9.3 - prettier-plugin-sort-json@4.2.0(prettier@3.8.0): + prettier-plugin-sort-json@4.2.0(prettier@3.8.1): dependencies: - prettier: 3.8.0 + prettier: 3.8.1 - prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.48.0): + prettier-plugin-svelte@3.4.1(prettier@3.8.1)(svelte@5.50.0): dependencies: - prettier: 3.8.0 - svelte: 5.48.0 + prettier: 3.8.1 + svelte: 5.50.0 - prettier@3.8.0: {} + prettier@3.8.1: {} pretty-error@4.0.0: dependencies: @@ -24508,7 +23307,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.10.9 + '@types/node': 24.10.13 long: 5.3.2 protobufjs@8.0.0: @@ -24523,7 +23322,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.10.9 + '@types/node': 24.10.13 long: 5.3.2 protocol-buffers-schema@3.6.0: {} @@ -24622,9 +23421,9 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-dom@19.2.3(react@19.2.3): + react-dom@19.2.4(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 scheduler: 0.27.0 react-email@4.3.2: @@ -24704,7 +23503,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - react@19.2.3: {} + react@19.2.4: {} read-cache@1.0.0: dependencies: @@ -25058,14 +23857,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0): + runed@0.35.1(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.48.0 + svelte: 5.50.0 optionalDependencies: - '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) rw@1.3.3: {} @@ -25156,12 +23955,14 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 semver@6.3.1: {} semver@7.7.3: {} + semver@7.7.4: {} + send@0.19.2: dependencies: debug: 2.6.9 @@ -25242,7 +24043,7 @@ snapshots: set-blocking@2.0.0: {} - set-cookie-parser@2.7.2: {} + set-cookie-parser@3.0.1: {} set-function-length@1.2.2: dependencies: @@ -25277,8 +24078,8 @@ snapshots: '@img/colour': 1.0.0 detect-libc: 2.1.2 node-addon-api: 8.5.0 - node-gyp: 12.1.0 - semver: 7.7.3 + node-gyp: 12.2.0 + semver: 7.7.4 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -25432,7 +24233,7 @@ snapshots: dependencies: accepts: 1.3.8 base64id: 2.0.0 - cors: 2.8.5 + cors: 2.8.6 debug: 4.4.3 engine.io: 6.6.5 socket.io-adapter: 2.5.6 @@ -25527,7 +24328,7 @@ snapshots: cpu-features: 0.0.10 nan: 2.24.0 - ssri@13.0.0: + ssri@13.0.1: dependencies: minipass: 7.1.2 @@ -25621,8 +24422,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.1.2: {} - strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 @@ -25695,23 +24494,23 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome@3.3.5(svelte@5.48.0): + svelte-awesome@3.3.5(svelte@5.50.0): dependencies: - svelte: 5.48.0 + svelte: 5.50.0 - svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.48.0)(typescript@5.9.3): + svelte-check@4.3.6(picomatch@4.0.3)(svelte@5.50.0)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.48.0 + svelte: 5.50.0 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.1(svelte@5.48.0): + svelte-eslint-parser@1.4.1(svelte@5.50.0): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -25720,7 +24519,7 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.1 optionalDependencies: - svelte: 5.48.0 + svelte: 5.50.0 svelte-floating-ui@1.5.8: dependencies: @@ -25733,7 +24532,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.48.0): + svelte-i18n@4.0.1(svelte@5.50.0): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -25741,10 +24540,10 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.48.0 + svelte: 5.50.0 tiny-glob: 0.2.9 - svelte-jsoneditor@3.11.0(svelte@5.48.0): + svelte-jsoneditor@3.11.0(svelte@5.50.0): dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.1 @@ -25771,46 +24570,46 @@ snapshots: memoize-one: 6.0.0 natural-compare-lite: 1.4.0 sass: 1.97.1 - svelte: 5.48.0 - svelte-awesome: 3.3.5(svelte@5.48.0) + svelte: 5.50.0 + svelte-awesome: 3.3.5(svelte@5.50.0) svelte-select: 5.8.3 vanilla-picker: 2.12.3 - svelte-maplibre@1.2.5(svelte@5.48.0): + svelte-maplibre@1.2.6(svelte@5.50.0): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 - maplibre-gl: 5.16.0 + maplibre-gl: 5.17.0 pmtiles: 3.2.1 - svelte: 5.48.0 + svelte: 5.50.0 - svelte-parse-markup@0.1.5(svelte@5.48.0): + svelte-parse-markup@0.1.5(svelte@5.50.0): dependencies: - svelte: 5.48.0 + svelte: 5.50.0 - svelte-persisted-store@0.12.0(svelte@5.48.0): + svelte-persisted-store@0.12.0(svelte@5.50.0): dependencies: - svelte: 5.48.0 + svelte: 5.50.0 svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) + runed: 0.35.1(@sveltejs/kit@2.50.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.50.0) style-to-object: 1.0.14 - svelte: 5.48.0 + svelte: 5.50.0 transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.48.0: + svelte@5.50.0: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0) '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.2 @@ -25818,7 +24617,7 @@ snapshots: clsx: 2.1.1 devalue: 5.6.2 esm-env: 1.2.2 - esrap: 2.2.1 + esrap: 2.2.3 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -25958,7 +24757,7 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - tar@7.5.2: + tar@7.5.7: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -25966,16 +24765,16 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))): + terser-webpack-plugin@5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)) optionalDependencies: - '@swc/core': 1.15.8(@swc/helpers@0.5.17) + '@swc/core': 1.15.11(@swc/helpers@0.5.17) terser-webpack-plugin@5.3.16(webpack@5.104.1): dependencies: @@ -26174,7 +24973,7 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 tapable: 2.3.0 tsconfig-paths: 4.2.0 @@ -26190,7 +24989,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.2 + esbuild: 0.27.3 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -26226,12 +25025,12 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -26241,7 +25040,7 @@ snapshots: ua-is-frozen@0.1.2: {} - ua-parser-js@2.0.8: + ua-parser-js@2.0.9: dependencies: detect-europe-js: 0.1.2 is-standalone-pwa: 0.1.1 @@ -26262,8 +25061,6 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.21.0: {} - undici-types@7.16.0: {} undici@7.18.0: {} @@ -26368,10 +25165,10 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.55.1): + unplugin-swc@1.5.9(@swc/core@1.15.11(@swc/helpers@0.5.17))(rollup@4.55.1): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.55.1) - '@swc/core': 1.15.8(@swc/helpers@0.5.17) + '@swc/core': 1.15.11(@swc/helpers@0.5.17) load-tsconfig: 0.2.5 unplugin: 2.3.11 transitivePeerDependencies: @@ -26384,7 +25181,7 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.2.2(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 @@ -26403,7 +25200,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.7.3 + semver: 7.7.4 semver-diff: 4.0.0 xdg-basedir: 5.1.0 @@ -26511,13 +25308,13 @@ snapshots: - rollup - supports-color - vite-node@3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -26532,13 +25329,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -26553,27 +25350,26 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.1.0(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - optionalDependencies: - vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: - esbuild: 0.27.2 + esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.9 + '@types/node': 24.10.13 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -26582,16 +25378,16 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: - esbuild: 0.27.2 + esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.9 + '@types/node': 25.2.3 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -26600,19 +25396,19 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitefu@1.1.1(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitefu@1.1.1(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -26630,13 +25426,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.10.9 - happy-dom: 20.3.0 + '@types/node': 24.10.13 + happy-dom: 20.5.0 jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti @@ -26652,11 +25448,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -26674,13 +25470,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.10.9 - happy-dom: 20.3.0 + '@types/node': 24.10.13 + happy-dom: 20.5.0 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti @@ -26696,11 +25492,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.2.3)(happy-dom@20.5.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -26718,14 +25514,14 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 25.0.9 - happy-dom: 20.3.0 - jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) + '@types/node': 25.2.3 + happy-dom: 20.5.0 + jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti - less @@ -26894,7 +25690,7 @@ snapshots: acorn-import-phases: 1.0.4(acorn@8.15.0) browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -26914,7 +25710,7 @@ snapshots: - esbuild - uglify-js - webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)): + webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -26926,7 +25722,7 @@ snapshots: acorn-import-phases: 1.0.4(acorn@8.15.0) browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -26938,7 +25734,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17))) + terser-webpack-plugin: 5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))) watchpack: 2.5.1 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -27008,9 +25804,9 @@ snapshots: dependencies: isexe: 2.0.0 - which@6.0.0: + which@6.0.1: dependencies: - isexe: 3.1.1 + isexe: 4.0.0 why-is-node-running@2.3.0: dependencies: @@ -27049,12 +25845,6 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 - wrap-ansi@9.0.2: - dependencies: - ansi-styles: 6.2.3 - string-width: 7.2.0 - strip-ansi: 7.1.2 - wrappy@1.0.2: {} write-file-atomic@3.0.3: diff --git a/server/package.json b/server/package.json index 70ef426557..80427642e5 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "2.5.5", + "version": "2.5.6", "description": "", "author": "", "private": true, @@ -45,14 +45,14 @@ "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.210.0", - "@opentelemetry/instrumentation-http": "^0.210.0", - "@opentelemetry/instrumentation-ioredis": "^0.58.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.56.0", - "@opentelemetry/instrumentation-pg": "^0.62.0", + "@opentelemetry/exporter-prometheus": "^0.211.0", + "@opentelemetry/instrumentation-http": "^0.211.0", + "@opentelemetry/instrumentation-ioredis": "^0.59.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.57.0", + "@opentelemetry/instrumentation-pg": "^0.63.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.210.0", + "@opentelemetry/sdk-node": "^0.211.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^0.5.0", "@react-email/render": "^1.1.2", @@ -69,7 +69,7 @@ "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", - "cron": "4.3.5", + "cron": "4.4.0", "exiftool-vendored": "^34.3.0", "express": "^5.1.0", "fast-glob": "^3.3.2", @@ -135,7 +135,7 @@ "@types/luxon": "^3.6.2", "@types/mock-fs": "^4.13.1", "@types/multer": "^2.0.0", - "@types/node": "^24.10.9", + "@types/node": "^24.10.11", "@types/nodemailer": "^7.0.0", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 7d622ea23d..49b779ca18 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -33,6 +33,7 @@ import { WebsocketRepository } from 'src/repositories/websocket.repository'; import { services } from 'src/services'; import { AuthService } from 'src/services/auth.service'; import { CliService } from 'src/services/cli.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; import { QueueService } from 'src/services/queue.service'; import { getKyselyConfig } from 'src/utils/database'; @@ -114,6 +115,7 @@ export class ApiModule extends BaseModule {} AppRepository, MaintenanceHealthRepository, MaintenanceWebsocketRepository, + DatabaseBackupService, MaintenanceWorkerService, ...commonMiddleware, { provide: APP_GUARD, useClass: MaintenanceAuthGuard }, diff --git a/server/src/commands/index.ts b/server/src/commands/index.ts index 2aef2e8c8b..2a2dd1857d 100644 --- a/server/src/commands/index.ts +++ b/server/src/commands/index.ts @@ -9,6 +9,7 @@ import { import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login'; import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login'; import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command'; +import { SchemaCheck } from 'src/commands/schema-check'; import { VersionCommand } from 'src/commands/version.command'; export const commandsAndQuestions = [ @@ -28,4 +29,5 @@ export const commandsAndQuestions = [ ChangeMediaLocationCommand, PromptMediaLocationQuestions, PromptConfirmMoveQuestions, + SchemaCheck, ]; diff --git a/server/src/commands/schema-check.ts b/server/src/commands/schema-check.ts new file mode 100644 index 0000000000..c6e90fd9ca --- /dev/null +++ b/server/src/commands/schema-check.ts @@ -0,0 +1,60 @@ +import { Command, CommandRunner } from 'nest-commander'; +import { ErrorMessages } from 'src/constants'; +import { CliService } from 'src/services/cli.service'; +import { asHuman } from 'src/sql-tools/schema-diff'; + +@Command({ + name: 'schema-check', + description: 'Verify database migrations and check for schema drift', +}) +export class SchemaCheck extends CommandRunner { + constructor(private service: CliService) { + super(); + } + + async run(): Promise { + try { + const { migrations, drift } = await this.service.schemaReport(); + + if (migrations.every((item) => item.status === 'applied')) { + console.log('Migrations are up to date'); + } else { + console.log('Migration issues detected:'); + for (const migration of migrations) { + switch (migration.status) { + case 'deleted': { + console.log(` - ${migration.name} was applied, but the file no longer exists on disk`); + break; + } + + case 'missing': { + console.log(` - ${migration.name} exists, but has not been applied to the database`); + break; + } + } + } + } + + if (drift.items.length === 0) { + console.log('\nNo schema drift detected'); + } else { + console.log(`\n${ErrorMessages.SchemaDrift}`); + for (const item of drift.items) { + console.log(` - ${item.type}: ` + asHuman(item)); + } + + console.log(` + +The below SQL is automatically generated and may be helpful for resolving drift. ** Use at your own risk! ** + +\`\`\`sql +${drift.asSql().join('\n')} +\`\`\` +`); + } + } catch (error) { + console.error(error); + console.error('Unable to debug migrations'); + } + } +} diff --git a/server/src/constants.ts b/server/src/constants.ts index 809c7e45a8..9ea5e134b6 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -4,6 +4,13 @@ import { dirname, join } from 'node:path'; import { SemVer } from 'semver'; import { ApiTag, DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum'; +export const ErrorMessages = { + InconsistentMediaLocation: + 'Detected an inconsistent media location. For more information, see https://docs.immich.app/errors#inconsistent-media-location', + SchemaDrift: `Detected schema drift. For more information, see https://docs.immich.app/errors#schema-drift`, + TypeOrmUpgrade: 'Invalid upgrade path. For more information, see https://docs.immich.app/errors/#typeorm-upgrade', +}; + export const POSTGRES_VERSION_RANGE = '>=14.0.0'; export const VECTORCHORD_VERSION_RANGE = '>=0.3 <2'; export const VECTORS_VERSION_RANGE = '>=0.2 <0.4'; diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index 8875127a25..1f91409e80 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -22,21 +22,39 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { SharedLinkCreateDto, SharedLinkEditDto, + SharedLinkLoginDto, SharedLinkPasswordDto, SharedLinkResponseDto, SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; import { ApiTag, ImmichCookie, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; import { respondWithCookie } from 'src/utils/response'; import { UUIDParamDto } from 'src/validation'; +const getAuthTokens = (cookies: Record | undefined) => { + return cookies?.[ImmichCookie.SharedLinkToken]?.split(',') || []; +}; + +const merge = (cookies: Record | undefined, token: string) => { + const authTokens = getAuthTokens(cookies); + if (!authTokens.includes(token)) { + authTokens.push(token); + } + + return authTokens.join(','); +}; + @ApiTags(ApiTag.SharedLinks) @Controller('shared-links') export class SharedLinkController { - constructor(private service: SharedLinkService) {} + constructor( + private service: SharedLinkService, + private logger: LoggingRepository, + ) {} @Get() @Authenticated({ permission: Permission.SharedLinkRead }) @@ -49,6 +67,28 @@ export class SharedLinkController { return this.service.getAll(auth, dto); } + @Post('login') + @Authenticated({ sharedLink: true }) + @Endpoint({ + summary: 'Shared link login', + description: 'Login to a password protected shared link', + history: new HistoryBuilder().added('v2.6.0').beta('v2.6.0'), + }) + async sharedLinkLogin( + @Auth() auth: AuthDto, + @Body() dto: SharedLinkLoginDto, + @Req() req: Request, + @Res({ passthrough: true }) res: Response, + @GetLoginDetails() loginDetails: LoginDetails, + ): Promise { + const { sharedLink, token } = await this.service.login(auth, dto); + + return respondWithCookie(res, sharedLink, { + isSecure: loginDetails.isSecure, + values: [{ key: ImmichCookie.SharedLinkToken, value: merge(req.cookies, token) }], + }); + } + @Get('me') @Authenticated({ sharedLink: true }) @Endpoint({ @@ -59,19 +99,19 @@ export class SharedLinkController { async getMySharedLink( @Auth() auth: AuthDto, @Query() dto: SharedLinkPasswordDto, - @Req() request: Request, + @Req() req: Request, @Res({ passthrough: true }) res: Response, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const sharedLinkToken = request.cookies?.[ImmichCookie.SharedLinkToken]; - if (sharedLinkToken) { - dto.token = sharedLinkToken; + if (dto.password) { + this.logger.deprecate( + 'Passing shared link password via query parameters is deprecated and will be removed in the next major release. Please use POST /shared-links/login instead.', + ); + + return this.sharedLinkLogin(auth, { password: dto.password }, req, res, loginDetails); } - const body = await this.service.getMine(auth, dto); - return respondWithCookie(res, body, { - isSecure: loginDetails.isSecure, - values: body.token ? [{ key: ImmichCookie.SharedLinkToken, value: body.token }] : [], - }); + + return this.service.getMine(auth, getAuthTokens(req.cookies)); } @Get(':id') diff --git a/server/src/dtos/asset-response.dto.spec.ts b/server/src/dtos/asset-response.dto.spec.ts index e71ffdadd2..ff3b3f6acd 100644 --- a/server/src/dtos/asset-response.dto.spec.ts +++ b/server/src/dtos/asset-response.dto.spec.ts @@ -1,14 +1,14 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { AssetEditAction } from 'src/dtos/editing.dto'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { faceStub } from 'test/fixtures/face.stub'; -import { personStub } from 'test/fixtures/person.stub'; +import { AssetFaceFactory } from 'test/factories/asset-face.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { PersonFactory } from 'test/factories/person.factory'; describe('mapAsset', () => { describe('peopleWithFaces', () => { it('should transform all faces when a person has multiple faces in the same image', () => { + const person = PersonFactory.create(); const face1 = { - ...faceStub.primaryFace1, boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, @@ -18,8 +18,6 @@ describe('mapAsset', () => { }; const face2 = { - ...faceStub.primaryFace1, - id: 'assetFaceId-second', boundingBoxX1: 300, boundingBoxY1: 400, boundingBoxX2: 400, @@ -28,16 +26,22 @@ describe('mapAsset', () => { imageHeight: 800, }; - const asset = { - ...assetStub.withCropEdit, - faces: [face1, face2], - exifInfo: { - exifImageWidth: 1000, - exifImageHeight: 800, - }, - }; + const asset = AssetFactory.from() + .face(face1, (builder) => builder.person(person)) + .face(face2, (builder) => builder.person(person)) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .edit({ + action: AssetEditAction.Crop, + parameters: { + width: 1512, + height: 1152, + x: 216, + y: 1512, + }, + }) + .build(); - const result = mapAsset(asset as any); + const result = mapAsset(asset); expect(result.people).toBeDefined(); expect(result.people).toHaveLength(1); @@ -61,32 +65,22 @@ describe('mapAsset', () => { }); it('should transform unassigned faces with edits and dimensions', () => { - const unassignedFace = { - ...faceStub.noPerson1, + const unassignedFace = AssetFaceFactory.create({ boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, boundingBoxY2: 200, imageWidth: 1000, imageHeight: 800, - }; + }); - const asset = { - ...assetStub.withCropEdit, - faces: [unassignedFace], - exifInfo: { - exifImageWidth: 1000, - exifImageHeight: 800, - }, - edits: [ - { - action: AssetEditAction.Crop, - parameters: { x: 50, y: 50, width: 500, height: 400 }, - }, - ], - }; + const asset = AssetFactory.from() + .face(unassignedFace) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .edit({ action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 500, height: 400 } }) + .build(); - const result = mapAsset(asset as any); + const result = mapAsset(asset); expect(result.unassignedFaces).toBeDefined(); expect(result.unassignedFaces).toHaveLength(1); @@ -101,10 +95,6 @@ describe('mapAsset', () => { it('should handle multiple people each with multiple faces', () => { const person1Face1 = { - ...faceStub.primaryFace1, - id: 'face-1-1', - person: personStub.withName, - personId: personStub.withName.id, boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, @@ -114,10 +104,6 @@ describe('mapAsset', () => { }; const person1Face2 = { - ...faceStub.primaryFace1, - id: 'face-1-2', - person: personStub.withName, - personId: personStub.withName.id, boundingBoxX1: 300, boundingBoxY1: 300, boundingBoxX2: 400, @@ -127,10 +113,6 @@ describe('mapAsset', () => { }; const person2Face1 = { - ...faceStub.mergeFace1, - id: 'face-2-1', - person: personStub.mergePerson, - personId: personStub.mergePerson.id, boundingBoxX1: 500, boundingBoxY1: 100, boundingBoxX2: 600, @@ -139,23 +121,22 @@ describe('mapAsset', () => { imageHeight: 800, }; - const asset = { - ...assetStub.withCropEdit, - faces: [person1Face1, person1Face2, person2Face1], - exifInfo: { - exifImageWidth: 1000, - exifImageHeight: 800, - }, - edits: [], - }; + const person = PersonFactory.create({ id: 'person-1' }); - const result = mapAsset(asset as any); + const asset = AssetFactory.from() + .face(person1Face1, (builder) => builder.person(person)) + .face(person1Face2, (builder) => builder.person(person)) + .face(person2Face1, (builder) => builder.person({ id: 'person-2' })) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .build(); + + const result = mapAsset(asset); expect(result.people).toBeDefined(); expect(result.people).toHaveLength(2); - const person1 = result.people!.find((p) => p.id === personStub.withName.id); - const person2 = result.people!.find((p) => p.id === personStub.mergePerson.id); + const person1 = result.people!.find((p) => p.id === 'person-1'); + const person2 = result.people!.find((p) => p.id === 'person-2'); expect(person1).toBeDefined(); expect(person1!.faces).toHaveLength(2); @@ -173,10 +154,6 @@ describe('mapAsset', () => { it('should combine faces of the same person into a single entry', () => { const face1 = { - ...faceStub.primaryFace1, - id: 'face-1', - person: personStub.withName, - personId: personStub.withName.id, boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, @@ -186,10 +163,6 @@ describe('mapAsset', () => { }; const face2 = { - ...faceStub.primaryFace1, - id: 'face-2', - person: personStub.withName, - personId: personStub.withName.id, boundingBoxX1: 300, boundingBoxY1: 300, boundingBoxX2: 400, @@ -198,24 +171,21 @@ describe('mapAsset', () => { imageHeight: 800, }; - const asset = { - ...assetStub.withCropEdit, - faces: [face1, face2], - exifInfo: { - exifImageWidth: 1000, - exifImageHeight: 800, - }, - edits: [], - }; + const person = PersonFactory.create(); - const result = mapAsset(asset as any); + const asset = AssetFactory.from() + .face(face1, (builder) => builder.person(person)) + .face(face2, (builder) => builder.person(person)) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .build(); + + const result = mapAsset(asset); expect(result.people).toBeDefined(); expect(result.people).toHaveLength(1); - const person = result.people![0]; - expect(person.id).toBe(personStub.withName.id); - expect(person.faces).toHaveLength(2); + expect(result.people![0].id).toBe(person.id); + expect(result.people![0].faces).toHaveLength(2); }); }); }); diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 1465f68953..b2ecc70a3a 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; import { SharedLink } from 'src/database'; -import { HistoryBuilder } from 'src/decorators'; +import { HistoryBuilder, Property } from 'src/decorators'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; export class SharedLinkSearchDto { @ValidateUUID({ optional: true, description: 'Filter by album ID' }) @@ -94,6 +94,11 @@ export class SharedLinkEditDto { changeExpiryTime?: boolean; } +export class SharedLinkLoginDto { + @ValidateString({ description: 'Shared link password', example: 'password' }) + password!: string; +} + export class SharedLinkPasswordDto { @ApiPropertyOptional({ example: 'password', description: 'Link password' }) @IsString() @@ -112,7 +117,10 @@ export class SharedLinkResponseDto { description!: string | null; @ApiProperty({ description: 'Has password' }) password!: string | null; - @ApiPropertyOptional({ description: 'Access token' }) + @Property({ + description: 'Access token', + history: new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0'), + }) token?: string | null; @ApiProperty({ description: 'Owner user ID' }) userId!: string; diff --git a/server/src/maintenance/maintenance-worker.controller.ts b/server/src/maintenance/maintenance-worker.controller.ts index 72527e27c0..162fa27257 100644 --- a/server/src/maintenance/maintenance-worker.controller.ts +++ b/server/src/maintenance/maintenance-worker.controller.ts @@ -34,12 +34,14 @@ import { FilenameParamDto } from 'src/validation'; import type { DatabaseBackupController as _DatabaseBackupController } from 'src/controllers/database-backup.controller'; import type { ServerController as _ServerController } from 'src/controllers/server.controller'; import { DatabaseBackupDeleteDto, DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; @Controller() export class MaintenanceWorkerController { constructor( private logger: LoggingRepository, private service: MaintenanceWorkerService, + private databaseBackupService: DatabaseBackupService, ) {} /** @@ -61,7 +63,7 @@ export class MaintenanceWorkerController { @Get('admin/database-backups') @MaintenanceRoute() listDatabaseBackups(): Promise { - return this.service.listBackups(); + return this.databaseBackupService.listBackups(); } /** @@ -74,7 +76,7 @@ export class MaintenanceWorkerController { @Res() res: Response, @Next() next: NextFunction, ) { - await sendFile(res, next, () => this.service.downloadBackup(filename), this.logger); + await sendFile(res, next, () => this.databaseBackupService.downloadBackup(filename), this.logger); } /** @@ -83,7 +85,7 @@ export class MaintenanceWorkerController { @Delete('admin/database-backups') @MaintenanceRoute() async deleteDatabaseBackup(@Body() dto: DatabaseBackupDeleteDto): Promise { - return this.service.deleteBackup(dto.backups); + return this.databaseBackupService.deleteBackup(dto.backups); } /** @@ -96,7 +98,7 @@ export class MaintenanceWorkerController { @UploadedFile() file: Express.Multer.File, ): Promise { - return this.service.uploadBackup(file); + return this.databaseBackupService.uploadBackup(file); } @Get('admin/maintenance/status') diff --git a/server/src/maintenance/maintenance-worker.service.spec.ts b/server/src/maintenance/maintenance-worker.service.spec.ts index 9fd8f38fcb..1d5bee62b0 100644 --- a/server/src/maintenance/maintenance-worker.service.spec.ts +++ b/server/src/maintenance/maintenance-worker.service.spec.ts @@ -1,23 +1,18 @@ -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { UnauthorizedException } from '@nestjs/common'; import { SignJWT } from 'jose'; -import { DateTime } from 'luxon'; -import { PassThrough, Readable } from 'node:stream'; -import { StorageCore } from 'src/cores/storage.core'; -import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; -import { automock, AutoMocked, getMocks, mockDuplex, mockSpawn, ServiceMocks } from 'test/utils'; - -function* mockData() { - yield ''; -} +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { automock, AutoMocked, getMocks, ServiceMocks } from 'test/utils'; describe(MaintenanceWorkerService.name, () => { let sut: MaintenanceWorkerService; let mocks: ServiceMocks; let maintenanceWebsocketRepositoryMock: AutoMocked; let maintenanceHealthRepositoryMock: AutoMocked; + let databaseBackupServiceMock: AutoMocked; beforeEach(() => { mocks = getMocks(); @@ -29,6 +24,20 @@ describe(MaintenanceWorkerService.name, () => { args: [mocks.logger], strict: false, }); + databaseBackupServiceMock = automock(DatabaseBackupService, { + args: [ + mocks.logger, + mocks.storage, + mocks.config, + mocks.systemMetadata, + mocks.process, + mocks.database, + mocks.cron, + mocks.job, + maintenanceHealthRepositoryMock, + ], + strict: false, + }); sut = new MaintenanceWorkerService( mocks.logger as never, @@ -40,6 +49,7 @@ describe(MaintenanceWorkerService.name, () => { mocks.storage as never, mocks.process, mocks.database as never, + databaseBackupServiceMock, ); sut.mock({ @@ -310,17 +320,6 @@ describe(MaintenanceWorkerService.name, () => { describe('action: restore database', () => { beforeEach(() => { mocks.database.tryLock.mockResolvedValueOnce(true); - - mocks.storage.readdir.mockResolvedValue([]); - mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); - mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', '')); - mocks.process.fork.mockImplementation(() => mockSpawn(0, 'Immich Server is listening', '')); - mocks.storage.rename.mockResolvedValue(); - mocks.storage.unlink.mockResolvedValue(); - mocks.storage.createPlainReadStream.mockReturnValue(Readable.from(mockData())); - mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); - mocks.storage.createGzip.mockReturnValue(new PassThrough()); - mocks.storage.createGunzip.mockReturnValue(new PassThrough()); }); it('should update maintenance mode state', async () => { @@ -341,21 +340,7 @@ describe(MaintenanceWorkerService.name, () => { }); }); - it('should fail to restore invalid backup', async () => { - await sut.runAction({ - action: MaintenanceAction.RestoreDatabase, - restoreBackupFilename: 'filename', - }); - - expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { - active: true, - action: MaintenanceAction.RestoreDatabase, - error: 'Error: Invalid backup file format!', - task: 'error', - }); - }); - - it('should successfully run a backup', async () => { + it('should defer to database backup service', async () => { await sut.runAction({ action: MaintenanceAction.RestoreDatabase, restoreBackupFilename: 'development-filename.sql', @@ -380,13 +365,10 @@ describe(MaintenanceWorkerService.name, () => { action: 'end', }, ); - - expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); - expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(3); }); - it('should fail if backup creation fails', async () => { - mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); + it('should forward errors from database backup service', async () => { + databaseBackupServiceMock.restoreDatabaseBackup.mockRejectedValue('Sample error'); await sut.runAction({ action: MaintenanceAction.RestoreDatabase, @@ -396,149 +378,16 @@ describe(MaintenanceWorkerService.name, () => { expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { active: true, action: MaintenanceAction.RestoreDatabase, - error: 'Error: pg_dump non-zero exit code (1)\nerror', + error: 'Sample error', task: 'error', }); - expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( - 'MaintenanceStatusV1', - expect.any(String), - expect.objectContaining({ - task: 'error', - }), - ); - }); - - it('should fail if restore itself fails', async () => { - mocks.process.spawnDuplexStream - .mockReturnValueOnce(mockDuplex('pg_dump', 0, 'data', '')) - .mockReturnValueOnce(mockDuplex('gzip', 0, 'data', '')) - .mockReturnValueOnce(mockDuplex('psql', 1, '', 'error')); - - await sut.runAction({ - action: MaintenanceAction.RestoreDatabase, - restoreBackupFilename: 'development-filename.sql', - }); - - expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'public', { active: true, action: MaintenanceAction.RestoreDatabase, - error: 'Error: psql non-zero exit code (1)\nerror', + error: 'Something went wrong, see logs!', task: 'error', }); - - expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( - 'MaintenanceStatusV1', - expect.any(String), - expect.objectContaining({ - task: 'error', - }), - ); - }); - - it('should rollback if database migrations fail', async () => { - mocks.database.runMigrations.mockRejectedValue(new Error('Migrations Error')); - - await sut.runAction({ - action: MaintenanceAction.RestoreDatabase, - restoreBackupFilename: 'development-filename.sql', - }); - - expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { - active: true, - action: MaintenanceAction.RestoreDatabase, - error: 'Error: Migrations Error', - task: 'error', - }); - - expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalledTimes(0); - expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); - }); - - it('should rollback if API healthcheck fails', async () => { - maintenanceHealthRepositoryMock.checkApiHealth.mockRejectedValue(new Error('Health Error')); - - await sut.runAction({ - action: MaintenanceAction.RestoreDatabase, - restoreBackupFilename: 'development-filename.sql', - }); - - expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { - active: true, - action: MaintenanceAction.RestoreDatabase, - error: 'Error: Health Error', - task: 'error', - }); - - expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); - expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); - }); - }); - - /** - * Backups - */ - - describe('listBackups', () => { - it('should give us all backups', async () => { - mocks.storage.readdir.mockResolvedValue([ - `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, - `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - 'immich-db-backup-1753789649000.sql.gz', - `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - ]); - mocks.storage.stat.mockResolvedValue({ size: 1024 } as any); - - await expect(sut.listBackups()).resolves.toMatchObject({ - backups: [ - { filename: 'immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, - { filename: 'immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, - { filename: 'immich-db-backup-1753789649000.sql.gz', filesize: 1024 }, - ], - }); - }); - }); - - describe('deleteBackup', () => { - it('should reject invalid file names', async () => { - await expect(sut.deleteBackup(['filename'])).rejects.toThrowError( - new BadRequestException('Invalid backup name!'), - ); - }); - - it('should unlink the target file', async () => { - await sut.deleteBackup(['filename.sql']); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename.sql`, - ); - }); - }); - - describe('uploadBackup', () => { - it('should reject invalid file names', async () => { - await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( - new BadRequestException('Invalid backup name!'), - ); - }); - - it('should write file', async () => { - await sut.uploadBackup({ originalname: 'path.sql.gz', buffer: 'buffer' } as never); - expect(mocks.storage.createOrOverwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', 'buffer'); - }); - }); - - describe('downloadBackup', () => { - it('should reject invalid file names', () => { - expect(() => sut.downloadBackup('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!')); - }); - - it('should get backup path', () => { - expect(sut.downloadBackup('hello.sql.gz')).toEqual( - expect.objectContaining({ - path: '/data/backups/hello.sql.gz', - }), - ); }); }); }); diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts index 6415693733..9ceb3caa43 100644 --- a/server/src/maintenance/maintenance-worker.service.ts +++ b/server/src/maintenance/maintenance-worker.service.ts @@ -25,19 +25,11 @@ import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { type ApiService as _ApiService } from 'src/services/api.service'; import { type BaseService as _BaseService } from 'src/services/base.service'; -import { type DatabaseBackupService as _DatabaseBackupService } from 'src/services/database-backup.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; import { type ServerService as _ServerService } from 'src/services/server.service'; import { type VersionService as _VersionService } from 'src/services/version.service'; import { MaintenanceModeState } from 'src/types'; import { getConfig } from 'src/utils/config'; -import { - deleteDatabaseBackup, - downloadDatabaseBackup, - listDatabaseBackups, - restoreDatabaseBackup, - uploadDatabaseBackup, -} from 'src/utils/database-backups'; -import { ImmichFileResponse } from 'src/utils/file'; import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; @@ -62,6 +54,7 @@ export class MaintenanceWorkerService { private storageRepository: StorageRepository, private processRepository: ProcessRepository, private databaseRepository: DatabaseRepository, + private databaseBackupService: DatabaseBackupService, ) { this.logger.setContext(this.constructor.name); } @@ -187,35 +180,6 @@ export class MaintenanceWorkerService { return '/usr/src/app/upload'; } - /** - * {@link _DatabaseBackupService.listBackups} - */ - async listBackups(): Promise<{ backups: { filename: string; filesize: number }[] }> { - const backups = await listDatabaseBackups(this.backupRepos); - return { backups }; - } - - /** - * {@link _DatabaseBackupService.deleteBackup} - */ - async deleteBackup(files: string[]): Promise { - return deleteDatabaseBackup(this.backupRepos, files); - } - - /** - * {@link _DatabaseBackupService.uploadBackup} - */ - async uploadBackup(file: Express.Multer.File): Promise { - return uploadDatabaseBackup(this.backupRepos, file); - } - - /** - * {@link _DatabaseBackupService.downloadBackup} - */ - downloadBackup(fileName: string): ImmichFileResponse { - return downloadDatabaseBackup(fileName); - } - private get secret() { if (!this.#secret) { throw new Error('Secret is not initialised yet.'); @@ -364,7 +328,7 @@ export class MaintenanceWorkerService { progress: 0, }); - await restoreDatabaseBackup(this.backupRepos, filename, (task, progress) => + await this.databaseBackupService.restoreDatabaseBackup(filename, (task, progress) => this.setStatus({ active: true, action: MaintenanceAction.RestoreDatabase, diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 356f5af8f6..59f0f12424 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -176,6 +176,7 @@ select where "asset_file"."assetId" = "asset"."id" and "asset_file"."type" = 'preview' + and "asset_file"."isEdited" = $1 ) as "previewPath" from "person" @@ -183,7 +184,7 @@ from inner join "asset" on "asset_face"."assetId" = "asset"."id" left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" where - "person"."id" = $1 + "person"."id" = $2 and "asset_face"."deletedAt" is null -- PersonRepository.reassignFace diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 55ed2c1176..17647d065d 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -19,7 +19,9 @@ import { GenerateSql } from 'src/decorators'; import { DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import 'src/schema'; // make sure all schema definitions are imported for schemaFromCode import { DB } from 'src/schema'; +import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools'; import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types'; import { vectorIndexQuery } from 'src/utils/database'; import { isValidInteger } from 'src/validation'; @@ -281,6 +283,27 @@ export class DatabaseRepository { return rows[0].db; } + getMigrations() { + return this.db.selectFrom('kysely_migrations').select(['name', 'timestamp']).orderBy('name', 'asc').execute(); + } + + async getSchemaDrift() { + const source = schemaFromCode({ overrides: true, namingStrategy: 'default' }); + const target = await schemaFromDatabase(this.db, {}); + + const drift = schemaDiff(source, target, { + tables: { ignoreExtra: true }, + constraints: { ignoreExtra: false }, + indexes: { ignoreExtra: true }, + triggers: { ignoreExtra: true }, + columns: { ignoreExtra: true }, + functions: { ignoreExtra: false }, + parameters: { ignoreExtra: true }, + }); + + return drift; + } + async getDimensionSize(table: string, column = 'embedding'): Promise { const { rows } = await sql<{ dimsize: number }>` SELECT atttypmod as dimsize diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 1334d1220f..3c36bf62db 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -106,7 +106,7 @@ export class MetadataRepository { readTags(path: string): Promise { const args = mimeTypes.isVideo(path) ? ['-ee'] : []; - return this.exiftool.read(path, args).catch((error) => { + return this.exiftool.read(path, { readArgs: args }).catch((error) => { this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`); return {}; }) as Promise; diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index b03112821b..85e75483c5 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -288,6 +288,7 @@ export class PersonRepository { .select('asset_file.path') .whereRef('asset_file.assetId', '=', 'asset.id') .where('asset_file.type', '=', sql.lit(AssetFileType.Preview)) + .where('asset_file.isEdited', '=', false) .as('previewPath'), ) .where('person.id', '=', id) diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 59c9f53d1a..4dc3d40312 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -168,6 +168,8 @@ export interface Migrations { } export interface DB { + kysely_migrations: { timestamp: string; name: string }; + activity: ActivityTable; album: AlbumTable; diff --git a/server/src/schema/migrations/1744910873969-InitialMigration.ts b/server/src/schema/migrations/1744910873969-InitialMigration.ts index b703a47536..530b084f83 100644 --- a/server/src/schema/migrations/1744910873969-InitialMigration.ts +++ b/server/src/schema/migrations/1744910873969-InitialMigration.ts @@ -1,4 +1,5 @@ import { Kysely, sql } from 'kysely'; +import { ErrorMessages } from 'src/constants'; import { DatabaseExtension } from 'src/enum'; import { getVectorExtension } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -16,9 +17,7 @@ export async function up(db: Kysely): Promise { rows: [lastMigration], } = await lastMigrationSql.execute(db); if (lastMigration?.name !== 'AddMissingIndex1744910873956') { - throw new Error( - 'Invalid upgrade path. For more information, see https://docs.immich.app/errors/#typeorm-upgrade', - ); + throw new Error(ErrorMessages.TypeOrmUpgrade); } logger.log('Database has up to date TypeORM migrations, skipping initial Kysely migration'); return; diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 0bcb87e2f4..84440fd4b6 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -9,13 +9,16 @@ import { AssetFile } from 'src/database'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto'; import { MapAsset } from 'src/dtos/asset-response.dto'; +import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { AssetMediaService } from 'src/services/asset-media.service'; import { UploadBody } from 'src/types'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; import { ImmichFileResponse } from 'src/utils/file'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -426,56 +429,52 @@ describe(AssetMediaService.name, () => { }); it('should handle a live photo', async () => { - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }) + .owner(authStub.user1.user) + .build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect( - sut.uploadAsset( - authStub.user1, - { ...createDto, livePhotoVideoId: 'live-photo-motion-asset' }, - fileStub.livePhotoStill, - ), + sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill), ).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: 'live-photo-still-asset', + id: asset.id, }); - expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); + expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id); expect(mocks.asset.update).not.toHaveBeenCalled(); }); it('should hide the linked motion asset', async () => { - mocks.asset.getById.mockResolvedValueOnce({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); - mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build(); + const asset = AssetFactory.create(); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect( - sut.uploadAsset( - authStub.user1, - { ...createDto, livePhotoVideoId: 'live-photo-motion-asset' }, - fileStub.livePhotoStill, - ), + sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill), ).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: 'live-photo-still-asset', + id: asset.id, }); - expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); + expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: 'live-photo-motion-asset', + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); }); it('should handle a sidecar file', async () => { - mocks.asset.getById.mockResolvedValueOnce(assetStub.image); - mocks.asset.create.mockResolvedValueOnce(assetStub.image); + const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build(); + mocks.asset.getById.mockResolvedValueOnce(asset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: assetStub.image.id, + id: asset.id, }); expect(mocks.storage.utimes).toHaveBeenCalledWith( @@ -501,13 +500,14 @@ describe(AssetMediaService.name, () => { }); it('should download a file', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getForOriginal.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForOriginal.mockResolvedValue(asset); - await expect(sut.downloadOriginal(authStub.admin, 'asset-1', {})).resolves.toEqual( + await expect(sut.downloadOriginal(authStub.admin, asset.id, {})).resolves.toEqual( new ImmichFileResponse({ - path: '/original/path.jpg', - fileName: 'asset-id.jpg', + path: asset.originalPath, + fileName: asset.originalFileName, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithCache, }), @@ -515,28 +515,19 @@ describe(AssetMediaService.name, () => { }); it('should download edited file by default when edits exist', async () => { - const editedAsset = { - ...assetStub.withCropEdit, - files: [ - ...assetStub.withCropEdit.files, - { - id: 'edited-file', - type: AssetFileType.FullSize, - path: '/uploads/user-id/fullsize/edited.jpg', - isEdited: true, - }, - ], - }; - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getForOriginal.mockResolvedValue({ - ...editedAsset, - editedPath: '/uploads/user-id/fullsize/edited.jpg', - }); + const editedAsset = AssetFactory.from() + .edit() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .file({ type: AssetFileType.FullSize, isEdited: true }) + .build(); - await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual( + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id])); + mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: editedAsset.files[3].path }); + + await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, {})).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/fullsize/edited.jpg', - fileName: 'asset-id.jpg', + path: editedAsset.files[3].path, + fileName: editedAsset.originalFileName, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithCache, }), @@ -544,28 +535,19 @@ describe(AssetMediaService.name, () => { }); it('should download edited file when edited=true', async () => { - const editedAsset = { - ...assetStub.withCropEdit, - files: [ - ...assetStub.withCropEdit.files, - { - id: 'edited-file', - type: AssetFileType.FullSize, - path: '/uploads/user-id/fullsize/edited.jpg', - isEdited: true, - }, - ], - }; - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getForOriginal.mockResolvedValue({ - ...editedAsset, - editedPath: '/uploads/user-id/fullsize/edited.jpg', - }); + const editedAsset = AssetFactory.from() + .edit() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .file({ type: AssetFileType.FullSize, isEdited: true }) + .build(); - await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual( + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id])); + mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: editedAsset.files[3].path }); + + await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, { edited: true })).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/fullsize/edited.jpg', - fileName: 'asset-id.jpg', + path: editedAsset.files[3].path, + fileName: editedAsset.originalFileName, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithCache, }), @@ -573,28 +555,18 @@ describe(AssetMediaService.name, () => { }); it('should not return the unedited version if requested using a shared link', async () => { - const editedAsset = { - ...assetStub.withCropEdit, - files: [ - ...assetStub.withCropEdit.files, - { - id: 'edited-file', - type: AssetFileType.FullSize, - path: '/uploads/user-id/fullsize/edited.jpg', - isEdited: true, - }, - ], - }; - mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForOriginal.mockResolvedValue({ - ...editedAsset, - editedPath: '/uploads/user-id/fullsize/edited.jpg', - }); + const fullsizeEdited = AssetFileFactory.create({ type: AssetFileType.FullSize, isEdited: true }); + const editedAsset = AssetFactory.from().edit({ action: AssetEditAction.Crop }).file(fullsizeEdited).build(); - await expect(sut.downloadOriginal(authStub.adminSharedLink, 'asset-id', { edited: false })).resolves.toEqual( + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([editedAsset.id])); + mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: fullsizeEdited.path }); + + await expect( + sut.downloadOriginal(AuthFactory.from().sharedLink().build(), editedAsset.id, { edited: false }), + ).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/fullsize/edited.jpg', - fileName: 'asset-id.jpg', + path: fullsizeEdited.path, + fileName: editedAsset.originalFileName, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithCache, }), @@ -602,25 +574,19 @@ describe(AssetMediaService.name, () => { }); it('should download original file when edited=false', async () => { - const editedAsset = { - ...assetStub.withCropEdit, - files: [ - ...assetStub.withCropEdit.files, - { - id: 'edited-file', - type: AssetFileType.FullSize, - path: '/uploads/user-id/fullsize/edited.jpg', - isEdited: true, - }, - ], - }; - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + const editedAsset = AssetFactory.from() + .edit() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .file({ type: AssetFileType.FullSize, isEdited: true }) + .build(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id])); mocks.asset.getForOriginal.mockResolvedValue(editedAsset); - await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: false })).resolves.toEqual( + await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, { edited: false })).resolves.toEqual( new ImmichFileResponse({ - path: '/original/path.jpg', - fileName: 'asset-id.jpg', + path: editedAsset.originalPath, + fileName: editedAsset.originalFileName, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithCache, }), @@ -638,129 +604,118 @@ describe(AssetMediaService.name, () => { }); it('should fall back to preview if the requested thumbnail file does not exist', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/path/to/preview.jpg' }); + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), - ).resolves.toEqual( + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual( new ImmichFileResponse({ - path: '/path/to/preview.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_thumbnail.jpg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, }), ); }); it('should get preview file', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/thumbs/path.jpg' }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }), - ).resolves.toEqual( + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.PREVIEW })).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/thumbs/path.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_preview.jpg', + fileName: `IMG_${asset.id}_preview.jpg`, }), ); }); it('should get thumbnail file', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/webp/path.ext' }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), - ).resolves.toEqual( + const asset = AssetFactory.from() + .file({ type: AssetFileType.Thumbnail, path: '/uploads/user-id/webp/path.ext' }) + .build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/webp/path.ext', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'application/octet-stream', - fileName: 'asset-id_thumbnail.ext', + fileName: `IMG_${asset.id}_thumbnail.ext`, }), ); - expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false); }); it('should get original thumbnail by default', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForThumbnail.mockResolvedValue({ - ...assetStub.image, - path: '/uploads/user-id/thumbs/original-thumbnail.jpg', - }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), - ).resolves.toEqual( + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/thumbs/original-thumbnail.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_thumbnail.jpg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, }), ); - expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false); }); it('should get edited thumbnail when edited=true', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForThumbnail.mockResolvedValue({ - ...assetStub.image, - path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', - }); + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail, isEdited: true }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL, edited: true }), + sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: true }), ).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_thumbnail.jpg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, }), ); - expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, true); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, true); }); it('should get original thumbnail when edited=false', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForThumbnail.mockResolvedValue({ - ...assetStub.image, - path: '/uploads/user-id/thumbs/original-thumbnail.jpg', - }); + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL, edited: false }), + sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: false }), ).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/thumbs/original-thumbnail.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_thumbnail.jpg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, }), ); - expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false); }); it('should not return the unedited version if requested using a shared link', async () => { - mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getForThumbnail.mockResolvedValue({ - ...assetStub.image, - path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', - }); + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); await expect( - sut.viewThumbnail(authStub.adminSharedLink, assetStub.image.id, { + sut.viewThumbnail(authStub.adminSharedLink, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: true, }), ).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_thumbnail.jpg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, }), ); - expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, true); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, true); }); }); @@ -774,18 +729,20 @@ describe(AssetMediaService.name, () => { }); it('should throw an error if the video asset could not be found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); - await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException); + await expect(sut.playbackVideo(authStub.admin, asset.id)).rejects.toBeInstanceOf(NotFoundException); }); it('should return the encoded video path if available', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id])); - mocks.asset.getForVideo.mockResolvedValue(assetStub.hasEncodedVideo); + const asset = AssetFactory.create({ encodedVideoPath: '/path/to/encoded/video.mp4' }); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForVideo.mockResolvedValue(asset); - await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual( + await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual( new ImmichFileResponse({ - path: assetStub.hasEncodedVideo.encodedVideoPath!, + path: asset.encodedVideoPath!, cacheControl: CacheControl.PrivateWithCache, contentType: 'video/mp4', }), @@ -793,12 +750,13 @@ describe(AssetMediaService.name, () => { }); it('should fall back to the original path', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id])); - mocks.asset.getForVideo.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForVideo.mockResolvedValue(asset); - await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual( + await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual( new ImmichFileResponse({ - path: assetStub.video.originalPath, + path: asset.originalPath, cacheControl: CacheControl.PrivateWithCache, contentType: 'application/octet-stream', }), diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index ff4dfa96ff..b677881cfe 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,16 +1,14 @@ import { BadRequestException } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetEditAction } from 'src/dtos/editing.dto'; -import { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; +import { AssetFileType, AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; import { AssetService } from 'src/services/asset.service'; import { AssetFactory } from 'test/factories/asset.factory'; import { AuthFactory } from 'test/factories/auth.factory'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { factory } from 'test/small.factory'; +import { factory, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; const stats: AssetStats = { @@ -34,14 +32,8 @@ describe(AssetService.name, () => { expect(sut).toBeDefined(); }); - const mockGetById = (assets: MapAsset[]) => { - mocks.asset.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId))); - }; - beforeEach(() => { ({ sut, mocks } = newTestService(AssetService)); - - mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]); }); describe('getStatistics', () => { @@ -79,7 +71,7 @@ describe(AssetService.name, () => { describe('getRandom', () => { it('should get own random assets', async () => { mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getRandom.mockResolvedValue([assetStub.image]); + mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]); await sut.getRandom(authStub.admin, 1); @@ -90,7 +82,7 @@ describe(AssetService.name, () => { const partner = factory.partner({ inTimeline: false }); const auth = factory.auth({ user: { id: partner.sharedWithId } }); - mocks.asset.getRandom.mockResolvedValue([assetStub.image]); + mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]); mocks.partner.getAll.mockResolvedValue([partner]); await sut.getRandom(auth, 1); @@ -102,7 +94,7 @@ describe(AssetService.name, () => { const partner = factory.partner({ inTimeline: true }); const auth = factory.auth({ user: { id: partner.sharedWithId } }); - mocks.asset.getRandom.mockResolvedValue([assetStub.image]); + mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]); mocks.partner.getAll.mockResolvedValue([partner]); await sut.getRandom(auth, 1); @@ -113,88 +105,90 @@ describe(AssetService.name, () => { describe('get', () => { it('should allow owner access', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.admin, assetStub.image.id); + await sut.get(authStub.admin, asset.id); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set([assetStub.image.id]), + new Set([asset.id]), undefined, ); }); it('should allow shared link access', async () => { - mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.adminSharedLink, assetStub.image.id); + await sut.get(authStub.adminSharedLink, asset.id); expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLink?.id, - new Set([assetStub.image.id]), + new Set([asset.id]), ); }); it('should strip metadata for shared link if exif is disabled', async () => { - mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.from().exif({ description: 'foo' }).build(); + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); const result = await sut.get( { ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } }, - assetStub.image.id, + asset.id, ); expect(result).toEqual(expect.objectContaining({ hasMetadata: false })); expect(result).not.toHaveProperty('exifInfo'); expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLink?.id, - new Set([assetStub.image.id]), + new Set([asset.id]), ); }); it('should allow partner sharing access', async () => { - mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.admin, assetStub.image.id); + await sut.get(authStub.admin, asset.id); - expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([assetStub.image.id]), - ); + expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([asset.id])); }); it('should allow shared album access', async () => { - mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.admin, assetStub.image.id); + await sut.get(authStub.admin, asset.id); - expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([assetStub.image.id]), - ); + expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([asset.id])); }); it('should throw an error for no access', async () => { - await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(authStub.admin, AssetFactory.create().id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.getById).not.toHaveBeenCalled(); }); it('should throw an error for an invalid shared link', async () => { - await expect(sut.get(authStub.adminSharedLink, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(authStub.adminSharedLink, AssetFactory.create().id)).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.access.asset.checkOwnerAccess).not.toHaveBeenCalled(); expect(mocks.asset.getById).not.toHaveBeenCalled(); }); it('should throw an error if the asset could not be found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); - await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(authStub.admin, asset.id)).rejects.toBeInstanceOf(BadRequestException); }); }); @@ -208,38 +202,41 @@ describe(AssetService.name, () => { }); it('should update the asset', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValue(assetStub.image); - mocks.asset.update.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); + mocks.asset.update.mockResolvedValue(asset); - await sut.update(authStub.admin, 'asset-1', { isFavorite: true }); + await sut.update(authStub.admin, asset.id, { isFavorite: true }); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, isFavorite: true }); }); it('should update the exif description', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValue(assetStub.image); - mocks.asset.update.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); + mocks.asset.update.mockResolvedValue(asset); - await sut.update(authStub.admin, 'asset-1', { description: 'Test description' }); + await sut.update(authStub.admin, asset.id, { description: 'Test description' }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( - { assetId: 'asset-1', description: 'Test description', lockedProperties: ['description'] }, + { assetId: asset.id, description: 'Test description', lockedProperties: ['description'] }, { lockedPropertiesBehavior: 'append' }, ); }); it('should update the exif rating', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValueOnce(assetStub.image); - mocks.asset.update.mockResolvedValueOnce(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(asset); + mocks.asset.update.mockResolvedValueOnce(asset); - await sut.update(authStub.admin, 'asset-1', { rating: 3 }); + await sut.update(authStub.admin, asset.id, { rating: 3 }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( { - assetId: 'asset-1', + assetId: asset.id, rating: 3, lockedProperties: ['rating'], }, @@ -249,74 +246,79 @@ describe(AssetService.name, () => { it('should fail linking a live video if the motion part could not be found', async () => { const auth = AuthFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); await expect( - sut.update(auth, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(auth, asset.id, { + livePhotoVideoId: 'unknown', }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: 'unknown', }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: 'unknown', visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: 'unknown', userId: auth.user.id, }); }); it('should fail linking a live video if the motion part is not a video', async () => { const auth = AuthFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from().owner(auth.user).build(); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(authStub.admin, asset.id, { + livePhotoVideoId: motionAsset.id, }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: motionAsset.id, userId: auth.user.id, }); }); it('should fail linking a live video if the motion part has a different owner', async () => { const auth = AuthFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(motionAsset); await expect( - sut.update(auth, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(auth, asset.id, { + livePhotoVideoId: motionAsset.id, }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: motionAsset.id, userId: auth.user.id, }); }); @@ -346,35 +348,40 @@ describe(AssetService.name, () => { it('should unlink a live video', async () => { const auth = AuthFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset); - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - mocks.asset.update.mockResolvedValueOnce(assetStub.image); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }) + .owner(auth.user) + .build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + const unlinkedAsset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(asset); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.update.mockResolvedValueOnce(unlinkedAsset); - await sut.update(auth, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }); + await sut.update(auth, asset.id, { livePhotoVideoId: null }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, + id: asset.id, livePhotoVideoId: null, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, - visibility: assetStub.livePhotoStillAsset.visibility, + id: motionAsset.id, + visibility: asset.visibility, }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, + assetId: motionAsset.id, userId: auth.user.id, }); }); it('should fail unlinking a live video if the asset could not be found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - // eslint-disable-next-line unicorn/no-useless-undefined - mocks.asset.getById.mockResolvedValueOnce(undefined); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(void 0); - await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(authStub.admin, asset.id, { livePhotoVideoId: null })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); @@ -555,7 +562,11 @@ describe(AssetService.name, () => { describe('handleAssetDeletion', () => { it('should clean up files', async () => { - const asset = assetStub.image; + const asset = AssetFactory.from() + .file({ type: AssetFileType.Thumbnail }) + .file({ type: AssetFileType.Preview }) + .file({ type: AssetFileType.FullSize }) + .build(); mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); @@ -565,12 +576,7 @@ describe(AssetService.name, () => { { name: JobName.FileDelete, data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - asset.originalPath, - ], + files: [...asset.files.map(({ path }) => path), asset.originalPath], }, }, ], @@ -579,91 +585,60 @@ describe(AssetService.name, () => { }); it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => { + const asset = AssetFactory.from() + .stack({}, (builder) => builder.asset()) + .build(); mocks.stack.delete.mockResolvedValue(); mocks.assetJob.getForAssetDeletion.mockResolvedValue({ - ...assetStub.primaryImage, - stack: { - id: 'stack-id', - primaryAssetId: assetStub.primaryImage.id, - assets: [{ id: 'one-asset' }], - }, + ...asset, + // TODO the specific query filters out the primary asset from `stack.assets`. This should be in a mapper eventually + stack: { ...asset.stack!, assets: asset.stack!.assets.filter(({ id }) => id !== asset.stack!.primaryAssetId) }, }); - await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true }); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); - expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id'); + expect(mocks.stack.delete).toHaveBeenCalledWith(asset.stackId); }); it('should delete a live photo', async () => { - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); mocks.asset.getLivePhotoCount.mockResolvedValue(0); await sut.handleAssetDeletion({ - id: assetStub.livePhotoStillAsset.id, + id: asset.id, deleteOnDisk: true, }); expect(mocks.job.queue.mock.calls).toEqual([ - [ - { - name: JobName.AssetDelete, - data: { - id: assetStub.livePhotoMotionAsset.id, - deleteOnDisk: true, - }, - }, - ], - [ - { - name: JobName.FileDelete, - data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - 'fake_path/asset_1.jpeg', - ], - }, - }, - ], + [{ name: JobName.AssetDelete, data: { id: motionAsset.id, deleteOnDisk: true } }], + [{ name: JobName.FileDelete, data: { files: [asset.originalPath] } }], ]); }); it('should not delete a live motion part if it is being used by another asset', async () => { + const asset = AssetFactory.create({ livePhotoVideoId: newUuid() }); mocks.asset.getLivePhotoCount.mockResolvedValue(2); - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); - await sut.handleAssetDeletion({ - id: assetStub.livePhotoStillAsset.id, - deleteOnDisk: true, - }); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); expect(mocks.job.queue.mock.calls).toEqual([ - [ - { - name: JobName.FileDelete, - data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - 'fake_path/asset_1.jpeg', - ], - }, - }, - ], + [{ name: JobName.FileDelete, data: { files: [`/data/library/IMG_${asset.id}.jpg`] } }], ]); }); it('should update usage', async () => { - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.image); - await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000); + const asset = AssetFactory.from().exif({ fileSizeInByte: 5000 }).build(); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, -5000); }); it('should fail if asset could not be found', async () => { mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0); - await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe( + await expect(sut.handleAssetDeletion({ id: AssetFactory.create().id, deleteOnDisk: true })).resolves.toBe( JobStatus.Failed, ); }); @@ -681,28 +656,30 @@ describe(AssetService.name, () => { it('should return OCR data for an asset', async () => { const ocr1 = factory.assetOcr({ text: 'Hello World' }); const ocr2 = factory.assetOcr({ text: 'Test Image' }); + const asset = AssetFactory.from().exif().build(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.ocr.getByAssetId.mockResolvedValue([ocr1, ocr2]); - mocks.asset.getById.mockResolvedValue(assetStub.image); + mocks.asset.getById.mockResolvedValue(asset); - await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([ocr1, ocr2]); + await expect(sut.getOcr(authStub.admin, asset.id)).resolves.toEqual([ocr1, ocr2]); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set(['asset-1']), + new Set([asset.id]), undefined, ); - expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith('asset-1'); + expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith(asset.id); }); it('should return empty array when no OCR data exists', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + const asset = AssetFactory.from().exif().build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.ocr.getByAssetId.mockResolvedValue([]); - mocks.asset.getById.mockResolvedValue(assetStub.image); - await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([]); + mocks.asset.getById.mockResolvedValue(asset); + await expect(sut.getOcr(authStub.admin, asset.id)).resolves.toEqual([]); - expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith('asset-1'); + expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith(asset.id); }); }); @@ -746,7 +723,7 @@ describe(AssetService.name, () => { describe('getUserAssetsByDeviceId', () => { it('get assets by device id', async () => { - const assets = [assetStub.image, assetStub.image1]; + const assets = [AssetFactory.create(), AssetFactory.create()]; mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId)); diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts deleted file mode 100644 index ea80dd5759..0000000000 --- a/server/src/services/backup.service.spec.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { DateTime } from 'luxon'; -import { PassThrough } from 'node:stream'; -import { defaults, SystemConfig } from 'src/config'; -import { StorageCore } from 'src/cores/storage.core'; -import { ImmichWorker, JobStatus, StorageFolder } from 'src/enum'; -import { BackupService } from 'src/services/backup.service'; -import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { mockDuplex, mockSpawn, newTestService, ServiceMocks } from 'test/utils'; -import { describe } from 'vitest'; - -describe(BackupService.name, () => { - let sut: BackupService; - let mocks: ServiceMocks; - - beforeEach(() => { - ({ sut, mocks } = newTestService(BackupService)); - }); - - it('should work', () => { - expect(sut).toBeDefined(); - }); - - describe('onBootstrapEvent', () => { - it('should init cron job and handle config changes', async () => { - mocks.database.tryLock.mockResolvedValue(true); - mocks.cron.create.mockResolvedValue(); - - await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); - - expect(mocks.cron.create).toHaveBeenCalled(); - }); - - it('should not initialize backup database cron job when lock is taken', async () => { - mocks.database.tryLock.mockResolvedValue(false); - - await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); - - expect(mocks.cron.create).not.toHaveBeenCalled(); - }); - - it('should not initialise backup database job when running on microservices', async () => { - mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); - await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); - - expect(mocks.cron.create).not.toHaveBeenCalled(); - }); - }); - - describe('onConfigUpdateEvent', () => { - beforeEach(async () => { - mocks.database.tryLock.mockResolvedValue(true); - mocks.cron.create.mockResolvedValue(); - - await sut.onConfigInit({ newConfig: defaults }); - }); - - it('should update cron job if backup is enabled', () => { - mocks.cron.update.mockResolvedValue(); - - sut.onConfigUpdate({ - oldConfig: defaults, - newConfig: { - backup: { - database: { - enabled: true, - cronExpression: '0 1 * * *', - }, - }, - } as SystemConfig, - }); - - expect(mocks.cron.update).toHaveBeenCalledWith({ name: 'backupDatabase', expression: '0 1 * * *', start: true }); - expect(mocks.cron.update).toHaveBeenCalled(); - }); - - it('should do nothing if instance does not have the backup database lock', async () => { - mocks.database.tryLock.mockResolvedValue(false); - await sut.onConfigInit({ newConfig: defaults }); - sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults }); - expect(mocks.cron.update).not.toHaveBeenCalled(); - }); - }); - - describe('cleanupDatabaseBackups', () => { - it('should do nothing if not reached keepLastAmount', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz']); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).not.toHaveBeenCalled(); - }); - - it('should remove failed backup files', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - //`immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, - mocks.storage.readdir.mockResolvedValue([ - 'immich-db-backup-123.sql.gz.tmp', - `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, - `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, - ]); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-123.sql.gz.tmp`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz.tmp`, - ); - }); - - it('should remove old backup files over keepLastAmount', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz', 'immich-db-backup-2.sql.gz']); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz`, - ); - }); - - it('should remove old backup files over keepLastAmount and failed backups', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.readdir.mockResolvedValue([ - `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, - `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - 'immich-db-backup-1753789649000.sql.gz', - `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - ]); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1753789649000.sql.gz`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz`, - ); - }); - }); - - describe('handleBackupDatabase', () => { - beforeEach(() => { - mocks.storage.readdir.mockResolvedValue([]); - mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); - mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', '')); - mocks.storage.rename.mockResolvedValue(); - mocks.storage.unlink.mockResolvedValue(); - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); - }); - - it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => { - // create a service instance with a URL connection that includes libpqcompat - const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true'; - const configMock = { - getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), - getWorker: () => ImmichWorker.Api, - isDev: () => false, - } as unknown as any; - - ({ sut, mocks } = newTestService(BackupService, { config: configMock })); - - mocks.storage.readdir.mockResolvedValue([]); - mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', '')); - mocks.storage.rename.mockResolvedValue(); - mocks.storage.unlink.mockResolvedValue(); - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); - mocks.database.getPostgresVersion.mockResolvedValue('14.10'); - - await sut.handleBackupDatabase(); - - expect(mocks.process.spawnDuplexStream).toHaveBeenCalled(); - const call = mocks.process.spawnDuplexStream.mock.calls[0]; - const args = call[1] as string[]; - expect(args).toMatchInlineSnapshot(` - [ - "postgresql://postgres:pwd@host:5432/immich?sslmode=require", - "--clean", - "--if-exists", - ] - `); - }); - - it('should run a database backup successfully', async () => { - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.Success); - expect(mocks.storage.createWriteStream).toHaveBeenCalled(); - }); - - it('should rename file on success', async () => { - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.Success); - expect(mocks.storage.rename).toHaveBeenCalled(); - }); - - it('should fail if pg_dump fails', async () => { - mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); - }); - - it('should not rename file if pgdump fails and gzip succeeds', async () => { - mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); - expect(mocks.storage.rename).not.toHaveBeenCalled(); - }); - - it('should fail if gzip fails', async () => { - mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 0, 'data', '')); - mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('gzip', 1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('gzip non-zero exit code (1)'); - }); - - it('should fail if write stream fails', async () => { - mocks.storage.createWriteStream.mockImplementation(() => { - throw new Error('error'); - }); - await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); - }); - - it('should fail if rename fails', async () => { - mocks.storage.rename.mockRejectedValue(new Error('error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); - }); - - it('should ignore unlink failing and still return failed job status', async () => { - mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); - mocks.storage.unlink.mockRejectedValue(new Error('error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); - expect(mocks.storage.unlink).toHaveBeenCalled(); - }); - - it.each` - postgresVersion | expectedVersion - ${'14.10'} | ${14} - ${'14.10.3'} | ${14} - ${'14.10 (Debian 14.10-1.pgdg120+1)'} | ${14} - ${'15.3.3'} | ${15} - ${'16.4.2'} | ${16} - ${'17.15.1'} | ${17} - ${'18.0.0'} | ${18} - `( - `should use pg_dump $expectedVersion with postgres version $postgresVersion`, - async ({ postgresVersion, expectedVersion }) => { - mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); - await sut.handleBackupDatabase(); - expect(mocks.process.spawnDuplexStream).toHaveBeenCalledWith( - `/usr/lib/postgresql/${expectedVersion}/bin/pg_dump`, - expect.any(Array), - expect.any(Object), - ); - }, - ); - it.each` - postgresVersion - ${'13.99.99'} - ${'19.0.0'} - `(`should fail if postgres version $postgresVersion is not supported`, async ({ postgresVersion }) => { - mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); - const result = await sut.handleBackupDatabase(); - expect(mocks.process.spawn).not.toHaveBeenCalled(); - expect(result).toBe(JobStatus.Failed); - }); - }); -}); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts deleted file mode 100644 index 637e968929..0000000000 --- a/server/src/services/backup.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import path from 'node:path'; -import { StorageCore } from 'src/cores/storage.core'; -import { OnEvent, OnJob } from 'src/decorators'; -import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum'; -import { ArgOf } from 'src/repositories/event.repository'; -import { BaseService } from 'src/services/base.service'; -import { - createDatabaseBackup, - isFailedDatabaseBackupName, - isValidDatabaseRoutineBackupName, - UnsupportedPostgresError, -} from 'src/utils/database-backups'; -import { handlePromiseError } from 'src/utils/misc'; - -@Injectable() -export class BackupService extends BaseService { - private backupLock = false; - - @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) - async onConfigInit({ - newConfig: { - backup: { database }, - }, - }: ArgOf<'ConfigInit'>) { - this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase); - - if (this.backupLock) { - this.cronRepository.create({ - name: 'backupDatabase', - expression: database.cronExpression, - onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.DatabaseBackup }), this.logger), - start: database.enabled, - }); - } - } - - @OnEvent({ name: 'ConfigUpdate', server: true }) - onConfigUpdate({ newConfig: { backup } }: ArgOf<'ConfigUpdate'>) { - if (!this.backupLock) { - return; - } - - this.cronRepository.update({ - name: 'backupDatabase', - expression: backup.database.cronExpression, - start: backup.database.enabled, - }); - } - - async cleanupDatabaseBackups() { - this.logger.debug(`Database Backup Cleanup Started`); - const { - backup: { database: config }, - } = await this.getConfig({ withCache: false }); - - const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); - const files = await this.storageRepository.readdir(backupsFolder); - const backups = files - .filter((filename) => isValidDatabaseRoutineBackupName(filename)) - .toSorted() - .toReversed(); - const failedBackups = files.filter((filename) => isFailedDatabaseBackupName(filename)); - - const toDelete = backups.slice(config.keepLastAmount); - toDelete.push(...failedBackups); - - for (const file of toDelete) { - await this.storageRepository.unlink(path.join(backupsFolder, file)); - } - this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`); - } - - @OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase }) - async handleBackupDatabase(): Promise { - try { - await createDatabaseBackup(this.backupRepos); - } catch (error) { - if (error instanceof UnsupportedPostgresError) { - return JobStatus.Failed; - } - - throw error; - } - - await this.cleanupDatabaseBackups(); - return JobStatus.Success; - } - - private get backupRepos() { - return { - logger: this.logger, - storage: this.storageRepository, - config: this.configRepository, - process: this.processRepository, - database: this.databaseRepository, - }; - } -} diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index ce62f98aa1..479fd130a6 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -1,15 +1,59 @@ import { Injectable } from '@nestjs/common'; -import { isAbsolute } from 'node:path'; +import { isAbsolute, join } from 'node:path'; import { SALT_ROUNDS } from 'src/constants'; import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { schemaDiff } from 'src/sql-tools'; import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; +export type SchemaReport = { + migrations: MigrationStatus[]; + drift: ReturnType; +}; + +type MigrationStatus = { + name: string; + status: 'applied' | 'missing' | 'deleted'; +}; + @Injectable() export class CliService extends BaseService { + async schemaReport(): Promise { + // eslint-disable-next-line unicorn/prefer-module + const allFiles = await this.storageRepository.readdir(join(__dirname, '../schema/migrations')); + const files = allFiles.filter((file) => file.endsWith('.js')).map((file) => file.slice(0, -3)); + const rows = await this.databaseRepository.getMigrations(); + const filesSet = new Set(files); + const rowsSet = new Set(rows.map((item) => item.name)); + const combined = [...filesSet, ...rowsSet].toSorted(); + + const migrations: MigrationStatus[] = []; + + for (const name of combined) { + if (filesSet.has(name) && rowsSet.has(name)) { + migrations.push({ name, status: 'applied' }); + continue; + } + + if (filesSet.has(name) && !rowsSet.has(name)) { + migrations.push({ name, status: 'missing' }); + continue; + } + + if (!filesSet.has(name) && rowsSet.has(name)) { + migrations.push({ name, status: 'deleted' }); + continue; + } + } + + const drift = await this.databaseRepository.getSchemaDrift(); + + return { migrations, drift }; + } + async listUsers(): Promise { const users = await this.userRepository.getList({ withDeleted: true }); return users.map((user) => mapUserAdmin(user)); diff --git a/server/src/services/database-backup.service.spec.ts b/server/src/services/database-backup.service.spec.ts index 4d68b02325..9ca37200b7 100644 --- a/server/src/services/database-backup.service.spec.ts +++ b/server/src/services/database-backup.service.spec.ts @@ -1,23 +1,594 @@ import { BadRequestException } from '@nestjs/common'; import { DateTime } from 'luxon'; +import { PassThrough, Readable } from 'node:stream'; +import { defaults, SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; -import { StorageFolder } from 'src/enum'; +import { ImmichWorker, JobStatus, StorageFolder } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { DatabaseBackupService } from 'src/services/database-backup.service'; -import { MaintenanceService } from 'src/services/maintenance.service'; -import { newTestService, ServiceMocks } from 'test/utils'; +import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { automock, AutoMocked, getMocks, mockDuplex, mockSpawn, ServiceMocks } from 'test/utils'; -describe(MaintenanceService.name, () => { +describe(DatabaseBackupService.name, () => { let sut: DatabaseBackupService; let mocks: ServiceMocks; + let maintenanceHealthRepositoryMock: AutoMocked; beforeEach(() => { - ({ sut, mocks } = newTestService(DatabaseBackupService)); + mocks = getMocks(); + maintenanceHealthRepositoryMock = automock(MaintenanceHealthRepository, { + args: [mocks.logger], + strict: false, + }); + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + mocks.config, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + maintenanceHealthRepositoryMock as never, + ); }); it('should work', () => { expect(sut).toBeDefined(); }); + describe('onBootstrapEvent', () => { + it('should init cron job and handle config changes', async () => { + mocks.database.tryLock.mockResolvedValue(true); + mocks.cron.create.mockResolvedValue(); + + await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); + + expect(mocks.cron.create).toHaveBeenCalled(); + }); + + it('should not initialize backup database cron job when lock is taken', async () => { + mocks.database.tryLock.mockResolvedValue(false); + + await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); + + expect(mocks.cron.create).not.toHaveBeenCalled(); + }); + + it('should not initialise backup database job when running on microservices', async () => { + mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); + await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); + + expect(mocks.cron.create).not.toHaveBeenCalled(); + }); + }); + + describe('onConfigUpdateEvent', () => { + beforeEach(async () => { + mocks.database.tryLock.mockResolvedValue(true); + mocks.cron.create.mockResolvedValue(); + + await sut.onConfigInit({ newConfig: defaults }); + }); + + it('should update cron job if backup is enabled', () => { + mocks.cron.update.mockResolvedValue(); + + sut.onConfigUpdate({ + oldConfig: defaults, + newConfig: { + backup: { + database: { + enabled: true, + cronExpression: '0 1 * * *', + }, + }, + } as SystemConfig, + }); + + expect(mocks.cron.update).toHaveBeenCalledWith({ name: 'backupDatabase', expression: '0 1 * * *', start: true }); + expect(mocks.cron.update).toHaveBeenCalled(); + }); + + it('should do nothing if instance does not have the backup database lock', async () => { + mocks.database.tryLock.mockResolvedValue(false); + await sut.onConfigInit({ newConfig: defaults }); + sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults }); + expect(mocks.cron.update).not.toHaveBeenCalled(); + }); + }); + + describe('cleanupDatabaseBackups', () => { + it('should do nothing if not reached keepLastAmount', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz']); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).not.toHaveBeenCalled(); + }); + + it('should remove failed backup files', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + //`immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, + mocks.storage.readdir.mockResolvedValue([ + 'immich-db-backup-123.sql.gz.tmp', + `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + ]); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-123.sql.gz.tmp`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz.tmp`, + ); + }); + + it('should remove old backup files over keepLastAmount', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz', 'immich-db-backup-2.sql.gz']); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz`, + ); + }); + + it('should remove old backup files over keepLastAmount and failed backups', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.readdir.mockResolvedValue([ + `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + 'immich-db-backup-1753789649000.sql.gz', + `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + ]); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1753789649000.sql.gz`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz`, + ); + }); + }); + + describe('handleBackupDatabase / createDatabaseBackup', () => { + beforeEach(() => { + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex()('command', 0, 'data', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + }); + + it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => { + // create a service instance with a URL connection that includes libpqcompat + const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex()('command', 0, 'data', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.database.getPostgresVersion.mockResolvedValue('14.10'); + + await sut.handleBackupDatabase(); + + expect(mocks.process.spawnDuplexStream).toHaveBeenCalled(); + const call = mocks.process.spawnDuplexStream.mock.calls[0]; + const args = call[1] as string[]; + expect(args).toMatchInlineSnapshot(` + [ + "postgresql://postgres:pwd@host:5432/immich?sslmode=require", + "--clean", + "--if-exists", + ] + `); + }); + + it('should run a database backup successfully', async () => { + const result = await sut.handleBackupDatabase(); + expect(result).toBe(JobStatus.Success); + expect(mocks.storage.createWriteStream).toHaveBeenCalled(); + }); + + it('should rename file on success', async () => { + const result = await sut.handleBackupDatabase(); + expect(result).toBe(JobStatus.Success); + expect(mocks.storage.rename).toHaveBeenCalled(); + }); + + it('should fail if pg_dump fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); + }); + + it('should not rename file if pgdump fails and gzip succeeds', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); + expect(mocks.storage.rename).not.toHaveBeenCalled(); + }); + + it('should fail if gzip fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('gzip', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('gzip non-zero exit code (1)'); + }); + + it('should fail if write stream fails', async () => { + mocks.storage.createWriteStream.mockImplementation(() => { + throw new Error('error'); + }); + await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); + }); + + it('should fail if rename fails', async () => { + mocks.storage.rename.mockRejectedValue(new Error('error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); + }); + + it('should ignore unlink failing and still return failed job status', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + mocks.storage.unlink.mockRejectedValue(new Error('error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); + expect(mocks.storage.unlink).toHaveBeenCalled(); + }); + + it.each` + postgresVersion | expectedVersion + ${'14.10'} | ${14} + ${'14.10.3'} | ${14} + ${'14.10 (Debian 14.10-1.pgdg120+1)'} | ${14} + ${'15.3.3'} | ${15} + ${'16.4.2'} | ${16} + ${'17.15.1'} | ${17} + ${'18.0.0'} | ${18} + `( + `should use pg_dump $expectedVersion with postgres version $postgresVersion`, + async ({ postgresVersion, expectedVersion }) => { + mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); + await sut.handleBackupDatabase(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledWith( + `/usr/lib/postgresql/${expectedVersion}/bin/pg_dump`, + expect.any(Array), + expect.any(Object), + ); + }, + ); + it.each` + postgresVersion + ${'13.99.99'} + ${'19.0.0'} + `(`should fail if postgres version $postgresVersion is not supported`, async ({ postgresVersion }) => { + mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); + const result = await sut.handleBackupDatabase(); + expect(mocks.process.spawn).not.toHaveBeenCalled(); + expect(result).toBe(JobStatus.Failed); + }); + }); + + describe('buildPostgresLaunchArguments', () => { + describe('default config', () => { + it('should generate pg_dump arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('pg_dump')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--username", + "postgres", + "--host", + "database", + "--port", + "5432", + "immich", + "--clean", + "--if-exists", + ], + "bin": "/usr/lib/postgresql/14/bin/pg_dump", + "databaseMajorVersion": 14, + "databasePassword": "postgres", + "databaseUsername": "postgres", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--username", + "postgres", + "--host", + "database", + "--port", + "5432", + "--dbname", + "immich", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "postgres", + "databaseUsername": "postgres", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql (single transaction) arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql', { singleTransaction: true })).resolves + .toMatchInlineSnapshot(` + { + "args": [ + "--username", + "postgres", + "--host", + "database", + "--port", + "5432", + "--dbname", + "immich", + "--single-transaction", + "--set", + "ON_ERROR_STOP=on", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "postgres", + "databaseUsername": "postgres", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + + describe('using custom parts', () => { + beforeEach(() => { + const configMock = { + getEnv: () => ({ + database: { + config: { + connectionType: 'parts', + host: 'myhost', + port: 1234, + username: 'mypg', + password: 'mypwd', + database: 'myimmich', + }, + skipMigrations: false, + }, + }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + }); + + it('should generate pg_dump arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('pg_dump')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--username", + "mypg", + "--host", + "myhost", + "--port", + "1234", + "myimmich", + "--clean", + "--if-exists", + ], + "bin": "/usr/lib/postgresql/14/bin/pg_dump", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql (single transaction) arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql', { singleTransaction: true })).resolves + .toMatchInlineSnapshot(` + { + "args": [ + "--username", + "mypg", + "--host", + "myhost", + "--port", + "1234", + "--dbname", + "myimmich", + "--single-transaction", + "--set", + "ON_ERROR_STOP=on", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + + describe('using URL', () => { + beforeEach(() => { + const dbUrl = 'postgresql://mypg:mypwd@myhost:1234/myimmich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + }); + + it('should generate pg_dump arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('pg_dump')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "postgresql://mypg:mypwd@myhost:1234/myimmich?sslmode=require", + "--clean", + "--if-exists", + ], + "bin": "/usr/lib/postgresql/14/bin/pg_dump", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql (single transaction) arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql', { singleTransaction: true })).resolves + .toMatchInlineSnapshot(` + { + "args": [ + "--dbname", + "postgresql://mypg:mypwd@myhost:1234/myimmich?sslmode=require", + "--single-transaction", + "--set", + "ON_ERROR_STOP=on", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + + describe('using bad URL', () => { + beforeEach(() => { + const dbUrl = 'post://gresql://mypg:myp@wd@myhos:t:1234/myimmich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + }); + + it('should fallback to reasonable defaults', async () => { + await expect(sut.buildPostgresLaunchArguments('psql')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--dbname", + "post://gresql//mypg:myp@wd@myhos:t:1234/myimmich?sslmode=require", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "", + "databaseUsername": "", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + }); + + describe('uploadBackup', () => { + it('should reject invalid file names', async () => { + await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( + new BadRequestException('Invalid backup name!'), + ); + }); + + it('should write file', async () => { + await sut.uploadBackup({ originalname: 'path.sql.gz', buffer: 'buffer' } as never); + expect(mocks.storage.createOrOverwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', 'buffer'); + }); + }); + + describe('downloadBackup', () => { + it('should reject invalid file names', () => { + expect(() => sut.downloadBackup('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!')); + }); + + it('should get backup path', () => { + expect(sut.downloadBackup('hello.sql.gz')).toEqual( + expect.objectContaining({ + path: '/data/backups/hello.sql.gz', + }), + ); + }); + }); + describe('listBackups', () => { it('should give us all backups', async () => { mocks.storage.readdir.mockResolvedValue([ @@ -54,30 +625,233 @@ describe(MaintenanceService.name, () => { }); }); - describe('uploadBackup', () => { - it('should reject invalid file names', async () => { - await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( - new BadRequestException('Invalid backup name!'), + describe('restoreDatabaseBackup', () => { + beforeEach(() => { + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.fork.mockImplementation(() => mockSpawn(0, 'Immich Server is listening', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.storage.createPlainReadStream.mockReturnValue(Readable.from(mockData())); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.storage.createGzip.mockReturnValue(new PassThrough()); + mocks.storage.createGunzip.mockReturnValue(new PassThrough()); + + const configMock = { + getEnv: () => ({ + database: { + config: { + connectionType: 'parts', + host: 'myhost', + port: 1234, + username: 'mypg', + password: 'mypwd', + database: 'myimmich', + }, + skipMigrations: false, + }, + }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + maintenanceHealthRepositoryMock, ); }); - it('should write file', async () => { - await sut.uploadBackup({ originalname: 'path.sql.gz', buffer: 'buffer' } as never); - expect(mocks.storage.createOrOverwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', 'buffer'); - }); - }); - - describe('downloadBackup', () => { - it('should reject invalid file names', () => { - expect(() => sut.downloadBackup('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!')); + it('should fail to restore invalid backup', async () => { + await expect(sut.restoreDatabaseBackup('filename')).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid backup file format!]`, + ); }); - it('should get backup path', () => { - expect(sut.downloadBackup('hello.sql.gz')).toEqual( + it('should successfully restore a backup', async () => { + let writtenToPsql = ''; + + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => { + return mockDuplex((chunk) => (writtenToPsql += chunk))('command', 0, 'data', ''); + }); + + const progress = vitest.fn(); + await sut.restoreDatabaseBackup('development-filename.sql', progress); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(3); + + expect(mocks.process.spawnDuplexStream).toHaveBeenLastCalledWith( + expect.stringMatching('/bin/psql'), + [ + '--username', + 'mypg', + '--host', + 'myhost', + '--port', + '1234', + '--dbname', + 'myimmich', + '--single-transaction', + '--set', + 'ON_ERROR_STOP=on', + '--echo-all', + '--output=/dev/null', + ], expect.objectContaining({ - path: '/data/backups/hello.sql.gz', + env: expect.objectContaining({ + PATH: expect.any(String), + PGPASSWORD: 'mypwd', + }), }), ); + + expect(writtenToPsql).toMatchInlineSnapshot(` + " + -- drop all other database connections + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); + + -- re-create the default schema + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + + -- restore access to schema + GRANT ALL ON SCHEMA public TO "mypg"; + GRANT ALL ON SCHEMA public TO public; + SELECT 1;" + `); + }); + + it('should generate pg_dumpall specific SQL instructions', async () => { + let writtenToPsql = ''; + + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => { + return mockDuplex((chunk) => (writtenToPsql += chunk))('command', 0, 'data', ''); + }); + + const progress = vitest.fn(); + await sut.restoreDatabaseBackup('development-v2.4.0-.sql', progress); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(3); + + expect(mocks.process.spawnDuplexStream).toHaveBeenLastCalledWith( + expect.stringMatching('/bin/psql'), + [ + '--username', + 'mypg', + '--host', + 'myhost', + '--port', + '1234', + '--dbname', + 'myimmich', + '--echo-all', + '--output=/dev/null', + ], + expect.objectContaining({ + env: expect.objectContaining({ + PATH: expect.any(String), + PGPASSWORD: 'mypwd', + }), + }), + ); + + expect(writtenToPsql).toMatchInlineSnapshot(String.raw` + " + -- drop all other database connections + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); + + \c postgres + SELECT 1;" + `); + }); + + it('should fail if backup creation fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + + const progress = vitest.fn(); + await expect(sut.restoreDatabaseBackup('development-filename.sql', progress)).rejects + .toThrowErrorMatchingInlineSnapshot(` + [Error: pg_dump non-zero exit code (1) + error] + `); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + }); + + it('should fail if restore itself fails', async () => { + mocks.process.spawnDuplexStream + .mockReturnValueOnce(mockDuplex()('pg_dump', 0, 'data', '')) + .mockReturnValueOnce(mockDuplex()('gzip', 0, 'data', '')) + .mockReturnValueOnce(mockDuplex()('psql', 1, '', 'error')); + + const progress = vitest.fn(); + await expect(sut.restoreDatabaseBackup('development-filename.sql', progress)).rejects + .toThrowErrorMatchingInlineSnapshot(` + [Error: psql non-zero exit code (1) + error] + `); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + }); + + it('should rollback if database migrations fail', async () => { + mocks.database.runMigrations.mockRejectedValue(new Error('Migrations Error')); + + const progress = vitest.fn(); + await expect( + sut.restoreDatabaseBackup('development-filename.sql', progress), + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Migrations Error]`); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalledTimes(0); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); + }); + + it('should rollback if API healthcheck fails', async () => { + maintenanceHealthRepositoryMock.checkApiHealth.mockRejectedValue(new Error('Health Error')); + + const progress = vitest.fn(); + await expect( + sut.restoreDatabaseBackup('development-filename.sql', progress), + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Health Error]`); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + expect(progress).toHaveBeenCalledWith('rollback', 0); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); }); }); }); + +function* mockData() { + yield 'SELECT 1;'; +} diff --git a/server/src/services/database-backup.service.ts b/server/src/services/database-backup.service.ts index 542e961b43..de7090fa83 100644 --- a/server/src/services/database-backup.service.ts +++ b/server/src/services/database-backup.service.ts @@ -1,43 +1,560 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Optional } from '@nestjs/common'; +import { debounce } from 'lodash'; +import { DateTime } from 'luxon'; +import path, { basename } from 'node:path'; +import { PassThrough, Readable, Writable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import semver from 'semver'; +import { serverVersion } from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { OnEvent, OnJob } from 'src/decorators'; import { DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; -import { BaseService } from 'src/services/base.service'; +import { CacheControl, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { ArgOf } from 'src/repositories/event.repository'; +import { JobRepository } from 'src/repositories/job.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; +import { getConfig } from 'src/utils/config'; import { - deleteDatabaseBackup, - downloadDatabaseBackup, - listDatabaseBackups, - uploadDatabaseBackup, + findDatabaseBackupVersion, + isFailedDatabaseBackupName, + isValidDatabaseBackupName, + isValidDatabaseRoutineBackupName, + UnsupportedPostgresError, } from 'src/utils/database-backups'; import { ImmichFileResponse } from 'src/utils/file'; +import { handlePromiseError } from 'src/utils/misc'; -/** - * This service is available outside of maintenance mode to manage maintenance mode - */ @Injectable() -export class DatabaseBackupService extends BaseService { - async listBackups(): Promise { - const backups = await listDatabaseBackups(this.backupRepos); - return { backups }; +export class DatabaseBackupService { + constructor( + private readonly logger: LoggingRepository, + private readonly storageRepository: StorageRepository, + private readonly configRepository: ConfigRepository, + private readonly systemMetadataRepository: SystemMetadataRepository, + private readonly processRepository: ProcessRepository, + private readonly databaseRepository: DatabaseRepository, + @Optional() + private readonly cronRepository: CronRepository, + @Optional() + private readonly jobRepository: JobRepository, + @Optional() + private readonly maintenanceHealthRepository: MaintenanceHealthRepository, + ) { + this.logger.setContext(this.constructor.name); } - deleteBackup(files: string[]): Promise { - return deleteDatabaseBackup(this.backupRepos, files); + private backupLock = false; + + @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) + async onConfigInit({ + newConfig: { + backup: { database }, + }, + }: ArgOf<'ConfigInit'>) { + if (!this.cronRepository || !this.jobRepository) { + return; + } + + this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase); + + if (this.backupLock) { + this.cronRepository.create({ + name: 'backupDatabase', + expression: database.cronExpression, + onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.DatabaseBackup }), this.logger), + start: database.enabled, + }); + } + } + + @OnEvent({ name: 'ConfigUpdate', server: true }) + onConfigUpdate({ newConfig: { backup } }: ArgOf<'ConfigUpdate'>) { + if (!this.cronRepository || !this.jobRepository || !this.backupLock) { + return; + } + + this.cronRepository.update({ + name: 'backupDatabase', + expression: backup.database.cronExpression, + start: backup.database.enabled, + }); + } + + @OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase }) + async handleBackupDatabase(): Promise { + try { + await this.createDatabaseBackup(); + } catch (error) { + if (error instanceof UnsupportedPostgresError) { + return JobStatus.Failed; + } + + throw error; + } + + await this.cleanupDatabaseBackups(); + return JobStatus.Success; + } + + async buildPostgresLaunchArguments( + bin: 'pg_dump' | 'pg_dumpall' | 'psql', + options: { + singleTransaction?: boolean; + } = {}, + ): Promise<{ + bin: string; + args: string[]; + databaseUsername: string; + databasePassword: string; + databaseVersion: string; + databaseMajorVersion?: number; + }> { + const { + database: { config: databaseConfig }, + } = this.configRepository.getEnv(); + const isUrlConnection = databaseConfig.connectionType === 'url'; + + const databaseVersion = await this.databaseRepository.getPostgresVersion(); + const databaseSemver = semver.coerce(databaseVersion); + const databaseMajorVersion = databaseSemver?.major; + + const args: string[] = []; + let databaseUsername; + + if (isUrlConnection) { + if (bin !== 'pg_dump') { + args.push('--dbname'); + } + + let url = databaseConfig.url; + if (URL.canParse(databaseConfig.url)) { + const parsedUrl = new URL(databaseConfig.url); + // remove known bad parameters + parsedUrl.searchParams.delete('uselibpqcompat'); + + databaseUsername = parsedUrl.username; + url = parsedUrl.toString(); + } + + // assume typical values if we can't parse URL or not present + databaseUsername ??= 'postgres'; + + args.push(url); + } else { + databaseUsername = databaseConfig.username; + + args.push( + '--username', + databaseUsername, + '--host', + databaseConfig.host, + '--port', + databaseConfig.port.toString(), + ); + + switch (bin) { + case 'pg_dumpall': { + args.push('--database'); + break; + } + case 'psql': { + args.push('--dbname'); + break; + } + } + + args.push(databaseConfig.database); + } + + switch (bin) { + case 'pg_dump': + case 'pg_dumpall': { + args.push('--clean', '--if-exists'); + break; + } + case 'psql': { + if (options.singleTransaction) { + args.push( + // don't commit any transaction on failure + '--single-transaction', + // exit with non-zero code on error + '--set', + 'ON_ERROR_STOP=on', + ); + } + + args.push( + // used for progress monitoring + '--echo-all', + '--output=/dev/null', + ); + break; + } + } + + if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <19.0.0')) { + this.logger.error(`Database Restore Failure: Unsupported PostgreSQL version: ${databaseVersion}`); + throw new UnsupportedPostgresError(databaseVersion); + } + + return { + bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`, + args, + databaseUsername, + databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password, + databaseVersion, + databaseMajorVersion, + }; + } + + async createDatabaseBackup(filenamePrefix: string = ''): Promise { + this.logger.debug(`Database Backup Started`); + + const { bin, args, databasePassword, databaseVersion, databaseMajorVersion } = + await this.buildPostgresLaunchArguments('pg_dump'); + + this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); + + const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`; + const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); + const temporaryFilePath = `${backupFilePath}.tmp`; + + try { + const pgdump = this.processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const gzip = this.processRepository.spawnDuplexStream('gzip', ['--rsyncable']); + const fileStream = this.storageRepository.createWriteStream(temporaryFilePath); + + await pipeline(pgdump, gzip, fileStream); + await this.storageRepository.rename(temporaryFilePath, backupFilePath); + } catch (error) { + this.logger.error(`Database Backup Failure: ${error}`); + await this.storageRepository + .unlink(temporaryFilePath) + .catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`)); + throw error; + } + + this.logger.log(`Database Backup Success`); + return backupFilePath; } async uploadBackup(file: Express.Multer.File): Promise { - return uploadDatabaseBackup(this.backupRepos, file); + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const fn = basename(file.originalname); + if (!isValidDatabaseBackupName(fn)) { + throw new BadRequestException('Invalid backup name!'); + } + + const filePath = path.join(backupsFolder, `uploaded-${fn}`); + await this.storageRepository.createOrOverwriteFile(filePath, file.buffer); } downloadBackup(fileName: string): ImmichFileResponse { - return downloadDatabaseBackup(fileName); - } + if (!isValidDatabaseBackupName(fileName)) { + throw new BadRequestException('Invalid backup name!'); + } + + const filePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName); - private get backupRepos() { return { - logger: this.logger, - storage: this.storageRepository, - config: this.configRepository, - process: this.processRepository, - database: this.databaseRepository, + path: filePath, + fileName, + cacheControl: CacheControl.PrivateWithoutCache, + contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql', }; } + + async listBackups(): Promise { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const files = await this.storageRepository.readdir(backupsFolder); + + const validFiles = files + .filter((fn) => isValidDatabaseBackupName(fn)) + .toSorted((a, b) => (a.startsWith('uploaded-') === b.startsWith('uploaded-') ? a.localeCompare(b) : 1)) + .toReversed(); + + const backups = await Promise.all( + validFiles.map(async (filename) => { + const stats = await this.storageRepository.stat(path.join(backupsFolder, filename)); + return { filename, filesize: stats.size }; + }), + ); + + return { + backups, + }; + } + + async deleteBackup(files: string[]): Promise { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + + if (files.some((filename) => !isValidDatabaseBackupName(filename))) { + throw new BadRequestException('Invalid backup name!'); + } + + await Promise.all(files.map((filename) => this.storageRepository.unlink(path.join(backupsFolder, filename)))); + } + + async cleanupDatabaseBackups() { + this.logger.debug(`Database Backup Cleanup Started`); + const { + backup: { database: config }, + } = await getConfig( + { + configRepo: this.configRepository, + metadataRepo: this.systemMetadataRepository, + logger: this.logger, + }, + { + withCache: false, + }, + ); + + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const files = await this.storageRepository.readdir(backupsFolder); + const backups = files + .filter((filename) => isValidDatabaseRoutineBackupName(filename)) + .toSorted() + .toReversed(); + const failedBackups = files.filter((filename) => isFailedDatabaseBackupName(filename)); + + const toDelete = backups.slice(config.keepLastAmount); + toDelete.push(...failedBackups); + + for (const file of toDelete) { + await this.storageRepository.unlink(path.join(backupsFolder, file)); + } + + this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`); + } + + async restoreDatabaseBackup( + filename: string, + progressCb?: (action: 'backup' | 'restore' | 'migrations' | 'rollback', progress: number) => void, + ): Promise { + this.logger.debug(`Database Restore Started`); + + let complete = false; + try { + if (!isValidDatabaseBackupName(filename)) { + throw new Error('Invalid backup file format!'); + } + + const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); + await this.storageRepository.stat(backupFilePath); // => check file exists + + let isPgClusterDump = false; + const version = findDatabaseBackupVersion(filename); + if (version && semver.satisfies(version, '<= 2.4')) { + isPgClusterDump = true; + } + + const { bin, args, databaseUsername, databasePassword, databaseMajorVersion } = + await this.buildPostgresLaunchArguments('psql', { + singleTransaction: !isPgClusterDump, + }); + + progressCb?.('backup', 0.05); + + const restorePointFilePath = await this.createDatabaseBackup('restore-point-'); + + this.logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`); + + let inputStream: Readable; + if (backupFilePath.endsWith('.gz')) { + const fileStream = this.storageRepository.createPlainReadStream(backupFilePath); + const gunzip = this.storageRepository.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + } else { + inputStream = this.storageRepository.createPlainReadStream(backupFilePath); + } + + const sqlStream = Readable.from(sql(inputStream, databaseUsername, isPgClusterDump)); + const psql = this.processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + this.logger.log(`Restore progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('restore', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + try { + progressCb?.('migrations', 0.9); + await this.databaseRepository.runMigrations(); + await this.maintenanceHealthRepository.checkApiHealth(); + } catch (error) { + progressCb?.('rollback', 0); + + const fileStream = this.storageRepository.createPlainReadStream(restorePointFilePath); + const gunzip = this.storageRepository.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + + const sqlStream = Readable.from(sqlRollback(inputStream, databaseUsername)); + const psql = this.processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + this.logger.log(`Rollback progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('rollback', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + throw error; + } + } catch (error) { + this.logger.error(`Database Restore Failure: ${error}`); + throw error; + } finally { + complete = true; + } + + this.logger.log(`Database Restore Success`); + } +} + +const SQL_DROP_CONNECTIONS = ` + -- drop all other database connections + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); +`; + +const SQL_RESET_SCHEMA = (username: string) => ` + -- re-create the default schema + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + + -- restore access to schema + GRANT ALL ON SCHEMA public TO "${username}"; + GRANT ALL ON SCHEMA public TO public; +`; + +async function* sql(inputStream: Readable, databaseUsername: string, isPgClusterDump: boolean) { + yield SQL_DROP_CONNECTIONS; + yield isPgClusterDump + ? // it is likely the dump contains SQL to try to drop the currently active + // database to ensure we have a fresh slate; if the `postgres` database exists + // then prefer to switch before continuing otherwise this will just silently fail + String.raw` + \c postgres + ` + : SQL_RESET_SCHEMA(databaseUsername); + + for await (const chunk of inputStream) { + yield chunk; + } +} + +async function* sqlRollback(inputStream: Readable, databaseUsername: string) { + yield SQL_DROP_CONNECTIONS; + yield SQL_RESET_SCHEMA(databaseUsername); + + for await (const chunk of inputStream) { + yield chunk; + } +} + +function createSqlProgressStreams(cb: (progress: number) => void) { + const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin'); + const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`); + + let readingStdin = false; + let sequenceIdx = 0; + + let linesSent = 0; + let linesProcessed = 0; + + const startedAt = +Date.now(); + const cbDebounced = debounce( + () => { + const progress = source.writableEnded + ? Math.min(1, linesProcessed / linesSent) + : // progress simulation while we're in an indeterminate state + Math.min(0.3, 0.1 + (Date.now() - startedAt) / 1e4); + cb(progress); + }, + 100, + { + maxWait: 100, + }, + ); + + let lastByte = -1; + const source = new PassThrough({ + transform(chunk, _encoding, callback) { + for (const byte of chunk) { + if (!readingStdin && byte === 10 && lastByte !== 10) { + linesSent += 1; + } + + lastByte = byte; + + const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER; + if (sequence[sequenceIdx] === byte) { + sequenceIdx += 1; + + if (sequence.length === sequenceIdx) { + sequenceIdx = 0; + readingStdin = !readingStdin; + } + } else { + sequenceIdx = 0; + } + } + + cbDebounced(); + this.push(chunk); + callback(); + }, + }); + + const sink = new Writable({ + write(chunk, _encoding, callback) { + for (const byte of chunk) { + if (byte === 10) { + linesProcessed++; + } + } + + cbDebounced(); + callback(); + }, + }); + + return [source, sink]; } diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index e30722d3d7..bae3a705a4 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -21,6 +21,11 @@ describe(DatabaseService.name, () => { extensionRange = '0.2.x'; mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VectorChord); mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange); + mocks.database.getSchemaDrift.mockResolvedValue({ + items: [], + asSql: () => [], + asHuman: () => [], + }); versionBelowRange = '0.1.0'; minVersionInRange = '0.2.0'; diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index 2ff0e0ca27..1b2289e6e3 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import semver from 'semver'; -import { EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants'; +import { ErrorMessages, EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants'; import { OnEvent } from 'src/decorators'; import { BootstrapEventPriority, DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -124,6 +124,17 @@ export class DatabaseService extends BaseService { const { database } = this.configRepository.getEnv(); if (!database.skipMigrations) { await this.databaseRepository.runMigrations(); + + this.logger.log('Checking for schema drift'); + const drift = await this.databaseRepository.getSchemaDrift(); + if (drift.items.length === 0) { + this.logger.log('No schema drift detected'); + } else { + this.logger.warn(`${ErrorMessages.SchemaDrift} or run \`immich-admin schema-check\``); + for (const warning of drift.asHuman()) { + this.logger.warn(` - ${warning}`); + } + } } await Promise.all([ this.databaseRepository.prewarm(VectorIndex.Clip), diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 7721b12ffc..ae010623d8 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -3,7 +3,6 @@ import { Readable } from 'node:stream'; import { DownloadResponseDto } from 'src/dtos/download.dto'; import { DownloadService } from 'src/services/download.service'; import { AssetFactory } from 'test/factories/asset.factory'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { vitest } from 'vitest'; @@ -37,21 +36,18 @@ describe(DownloadService.name, () => { finalize: vitest.fn(), stream: new Readable(), }; + const asset = AssetFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id, 'unknown-asset'])); + mocks.asset.getByIds.mockResolvedValue([asset]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id, 'unknown-asset'] })).resolves.toEqual({ stream: archiveMock.stream, }); expect(archiveMock.addFile).toHaveBeenCalledTimes(1); - expect(archiveMock.addFile).toHaveBeenNthCalledWith( - 1, - expect.stringContaining('/data/library/IMG_123.jpg'), - 'IMG_123.jpg', - ); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset.originalPath, asset.originalFileName); }); it('should log a warning if the original path could not be resolved', async () => { @@ -108,15 +104,14 @@ describe(DownloadService.name, () => { finalize: vitest.fn(), stream: new Readable(), }; + const asset1 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); + const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-1' }, - { ...assetStub.noResizePath, id: 'asset-2' }, - ]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id])); + mocks.asset.getByIds.mockResolvedValue([asset1, asset2]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({ stream: archiveMock.stream, }); @@ -131,15 +126,14 @@ describe(DownloadService.name, () => { finalize: vitest.fn(), stream: new Readable(), }; + const asset1 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); + const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-2' }, - { ...assetStub.noResizePath, id: 'asset-1' }, - ]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id])); + mocks.asset.getByIds.mockResolvedValue([asset2, asset1]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({ stream: archiveMock.stream, }); @@ -155,18 +149,17 @@ describe(DownloadService.name, () => { stream: new Readable(), }; - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-1', originalPath: '/path/to/symlink.jpg' }, - ]); + const asset = AssetFactory.create({ originalPath: '/path/to/symlink.jpg' }); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getByIds.mockResolvedValue([asset]); mocks.storage.realpath.mockResolvedValue('/path/to/realpath.jpg'); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id] })).resolves.toEqual({ stream: archiveMock.stream, }); - expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', asset.originalFileName); }); }); diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index e5ac9f82ba..0b216e8b8a 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -1,8 +1,9 @@ import { AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; import { DuplicateService } from 'src/services/duplicate.service'; import { SearchService } from 'src/services/search.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; +import { newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { beforeEach, vitest } from 'vitest'; @@ -38,19 +39,17 @@ describe(SearchService.name, () => { describe('getDuplicates', () => { it('should get duplicates', async () => { + const asset = AssetFactory.create(); mocks.duplicateRepository.getAll.mockResolvedValue([ { duplicateId: 'duplicate-id', - assets: [assetStub.image, assetStub.image], + assets: [asset, asset], }, ]); await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([ { duplicateId: 'duplicate-id', - assets: [ - expect.objectContaining({ id: assetStub.image.id }), - expect.objectContaining({ id: assetStub.image.id }), - ], + assets: [expect.objectContaining({ id: asset.id }), expect.objectContaining({ id: asset.id })], }, ]); }); @@ -101,7 +100,8 @@ describe(SearchService.name, () => { }); it('should queue missing assets', async () => { - mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([asset])); await sut.handleQueueSearchDuplicates({}); @@ -109,13 +109,14 @@ describe(SearchService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectDuplicates, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should queue all assets', async () => { - mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([asset])); await sut.handleQueueSearchDuplicates({ force: true }); @@ -123,7 +124,7 @@ describe(SearchService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectDuplicates, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); @@ -150,9 +151,7 @@ describe(SearchService.name, () => { }, }, }); - const id = assetStub.livePhotoMotionAsset.id; - - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: newUuid() }); expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); @@ -167,9 +166,7 @@ describe(SearchService.name, () => { }, }, }); - const id = assetStub.livePhotoMotionAsset.id; - - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: newUuid() }); expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); @@ -178,51 +175,49 @@ describe(SearchService.name, () => { it('should fail if asset is not found', async () => { mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue(void 0); - const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); + const asset = AssetFactory.create(); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Failed); - expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`); + expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${asset.id} not found`); }); it('should skip if asset is part of stack', async () => { - const id = assetStub.primaryImage.id; - mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, stackId: 'stack-id' }); + const asset = AssetFactory.from().stack().build(); + mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, stackId: asset.stackId }); - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Skipped); - expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is part of a stack, skipping`); }); it('should skip if asset is not visible', async () => { - const id = assetStub.livePhotoMotionAsset.id; - mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ - ...hasEmbedding, - visibility: AssetVisibility.Hidden, - }); + const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, ...asset }); - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Skipped); - expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is not visible, skipping`); }); it('should fail if asset is missing embedding', async () => { mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, embedding: null }); - const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); + const asset = AssetFactory.create(); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Failed); - expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is missing embedding`); }); it('should search for duplicates and update asset with duplicateId', async () => { mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue(hasEmbedding); - mocks.duplicateRepository.search.mockResolvedValue([ - { assetId: assetStub.image.id, distance: 0.01, duplicateId: null }, - ]); + const asset = AssetFactory.create(); + mocks.duplicateRepository.search.mockResolvedValue([{ assetId: asset.id, distance: 0.01, duplicateId: null }]); mocks.duplicateRepository.merge.mockResolvedValue(); - const expectedAssetIds = [assetStub.image.id, hasEmbedding.id]; + const expectedAssetIds = [asset.id, hasEmbedding.id]; const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id }); diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 2c2fb995c8..ba54474b71 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -7,7 +7,6 @@ import { AssetService } from 'src/services/asset.service'; import { AuditService } from 'src/services/audit.service'; import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthService } from 'src/services/auth.service'; -import { BackupService } from 'src/services/backup.service'; import { CliService } from 'src/services/cli.service'; import { DatabaseBackupService } from 'src/services/database-backup.service'; import { DatabaseService } from 'src/services/database.service'; @@ -58,7 +57,6 @@ export const services = [ AuditService, AuthService, AuthAdminService, - BackupService, CliService, DatabaseBackupService, DatabaseService, diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index c23b4f05df..a464c9e174 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -1,7 +1,8 @@ -import { ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetType, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { JobService } from 'src/services/job.service'; import { JobItem } from 'src/types'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(JobService.name, () => { @@ -55,22 +56,22 @@ describe(JobService.name, () => { { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], - stub: [assetStub.image], + stub: [AssetFactory.create({ id: 'asset-1' })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], - stub: [assetStub.video], + stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr], - stub: [assetStub.livePhotoStillAsset], + stub: [AssetFactory.create({ id: 'asset-1', livePhotoVideoId: newUuid() })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr, JobName.AssetEncodeVideo], - stub: [assetStub.video], + stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })], }, { item: { name: JobName.SmartSearch, data: { id: 'asset-1' } }, diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index dbff1ca467..d0c2d0a785 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -6,11 +6,11 @@ import { mapLibrary } from 'src/dtos/library.dto'; import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { makeMockWatcher } from 'test/repositories/storage.repository.mock'; -import { factory, newUuid } from 'test/small.factory'; +import { factory, newDate, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { vitest } from 'vitest'; @@ -306,13 +306,13 @@ describe(LibraryService.name, () => { it('should queue asset sync', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create({ libraryId: library.id, isExternal: true }); mocks.library.get.mockResolvedValue(library); mocks.storage.walk.mockImplementation(async function* generator() {}); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external])); + mocks.library.streamAssetIds.mockReturnValue(makeStream([asset])); mocks.asset.getLibraryAssetCount.mockResolvedValue(1); mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 0n }); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external])); const response = await sut.handleQueueSyncAssets({ id: library.id }); @@ -322,7 +322,7 @@ describe(LibraryService.name, () => { libraryId: library.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns, - assetIds: [assetStub.external.id], + assetIds: [asset.id], progressCounter: 1, totalAssets: 1, }, @@ -343,8 +343,9 @@ describe(LibraryService.name, () => { describe('handleSyncAssets', () => { it('should offline assets no longer on disk', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -352,20 +353,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: true, deletedAt: expect.anything(), }); }); it('should set assets deleted from disk as offline', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/data/user2'], exclusionPatterns: [], @@ -373,20 +375,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: true, deletedAt: expect.anything(), }); }); it('should do nothing with offline assets deleted from disk', async () => { + const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/data/user2'], exclusionPatterns: [], @@ -394,7 +397,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -403,8 +406,9 @@ describe(LibraryService.name, () => { }); it('should un-trash an asset previously marked as offline', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/original/'], exclusionPatterns: [], @@ -412,20 +416,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: false, deletedAt: null, }); }); it('should do nothing with offline asset if covered by exclusion pattern', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/original/'], exclusionPatterns: ['**/path.jpg'], @@ -433,8 +438,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -444,8 +449,9 @@ describe(LibraryService.name, () => { }); it('should do nothing with offline asset if not in import path', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/import/'], exclusionPatterns: [], @@ -453,8 +459,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -464,8 +470,9 @@ describe(LibraryService.name, () => { }); it('should do nothing with unchanged online assets', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -473,8 +480,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: asset.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -482,8 +489,9 @@ describe(LibraryService.name, () => { }); it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => { + const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -491,13 +499,13 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).toHaveBeenCalledWith( - [assetStub.trashedOffline.id], + [asset.id], expect.not.objectContaining({ fileCreatedAt: expect.anything(), }), @@ -505,8 +513,9 @@ describe(LibraryService.name, () => { }); it('should update with online assets that have changed', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -514,13 +523,9 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - if (assetStub.external.fileModifiedAt == null) { - throw new Error('fileModifiedAt is null'); - } + const mtime = new Date(asset.fileModifiedAt.getDate() + 1); - const mtime = new Date(assetStub.external.fileModifiedAt.getDate() + 1); - - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockResolvedValue({ mtime } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -529,7 +534,7 @@ describe(LibraryService.name, () => { { name: JobName.SidecarCheck, data: { - id: assetStub.external.id, + id: asset.id, source: 'upload', }, }, @@ -548,13 +553,14 @@ describe(LibraryService.name, () => { it('should import a new asset', async () => { const library = factory.library(); + const asset = AssetFactory.create(); const mockLibraryJob: ILibraryFileJob = { libraryId: library.id, paths: ['/data/user1/photo.jpg'], }; - mocks.asset.createAll.mockResolvedValue([assetStub.image]); + mocks.asset.createAll.mockResolvedValue([asset]); mocks.library.get.mockResolvedValue(library); await expect(sut.handleSyncFiles(mockLibraryJob)).resolves.toBe(JobStatus.Success); @@ -575,7 +581,7 @@ describe(LibraryService.name, () => { { name: JobName.SidecarCheck, data: { - id: assetStub.image.id, + id: asset.id, source: 'upload', }, }, @@ -602,7 +608,7 @@ describe(LibraryService.name, () => { it('should delete a library', async () => { const library = factory.library(); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.library.get.mockResolvedValue(library); await sut.delete(library.id); @@ -614,7 +620,7 @@ describe(LibraryService.name, () => { it('should allow an external library to be deleted', async () => { const library = factory.library(); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.library.get.mockResolvedValue(library); await sut.delete(library.id); @@ -630,7 +636,7 @@ describe(LibraryService.name, () => { it('should unwatch an external library when deleted', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); @@ -962,7 +968,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.storage.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); await sut.watchAll(); @@ -981,7 +987,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.storage.watch.mockImplementation( makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }), ); @@ -999,12 +1005,13 @@ describe(LibraryService.name, () => { it('should handle a file unlink event', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create(); mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset); mocks.storage.watch.mockImplementation( - makeMockWatcher({ items: [{ event: 'unlink', value: assetStub.image.originalPath }] }), + makeMockWatcher({ items: [{ event: 'unlink', value: asset.originalPath }] }), ); await sut.watchAll(); @@ -1013,16 +1020,17 @@ describe(LibraryService.name, () => { name: JobName.LibraryRemoveAsset, data: { libraryId: library.id, - paths: [assetStub.image.originalPath], + paths: [asset.originalPath], }, }); }); it('should handle an error event', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create({ libraryId: library.id, isExternal: true }); mocks.library.get.mockResolvedValue(library); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset); mocks.library.getAll.mockResolvedValue([library]); mocks.storage.watch.mockImplementation( makeMockWatcher({ @@ -1115,7 +1123,7 @@ describe(LibraryService.name, () => { const library = factory.library(); mocks.library.get.mockResolvedValue(library); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.image1])); + mocks.library.streamAssetIds.mockReturnValue(makeStream([AssetFactory.create()])); await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.Success); }); diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index 6dc56abf44..d58ae67140 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -1,7 +1,8 @@ import { MapService } from 'src/services/map.service'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { authStub } from 'test/fixtures/auth.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; +import { userStub } from 'test/fixtures/user.stub'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -15,36 +16,41 @@ describe(MapService.name, () => { describe('getMapMarkers', () => { it('should get geo information of assets', async () => { - const asset = assetStub.withLocation; + const auth = AuthFactory.create(); + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); const marker = { id: asset.id, - lat: asset.exifInfo!.latitude!, - lon: asset.exifInfo!.longitude!, - city: asset.exifInfo!.city, - state: asset.exifInfo!.state, - country: asset.exifInfo!.country, + lat: asset.exifInfo.latitude!, + lon: asset.exifInfo.longitude!, + city: asset.exifInfo.city, + state: asset.exifInfo.state, + country: asset.exifInfo.country, }; mocks.partner.getAll.mockResolvedValue([]); mocks.map.getMapMarkers.mockResolvedValue([marker]); - const markers = await sut.getMapMarkers(authStub.user1, {}); + const markers = await sut.getMapMarkers(auth, {}); expect(markers).toHaveLength(1); expect(markers[0]).toEqual(marker); }); it('should include partner assets', async () => { - const partner = factory.partner(); - const auth = factory.auth({ user: { id: partner.sharedWithId } }); + const auth = AuthFactory.create(); + const partner = factory.partner({ sharedWithId: auth.user.id }); - const asset = assetStub.withLocation; + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); const marker = { id: asset.id, - lat: asset.exifInfo!.latitude!, - lon: asset.exifInfo!.longitude!, - city: asset.exifInfo!.city, - state: asset.exifInfo!.state, - country: asset.exifInfo!.country, + lat: asset.exifInfo.latitude!, + lon: asset.exifInfo.longitude!, + city: asset.exifInfo.city, + state: asset.exifInfo.state, + country: asset.exifInfo.country, }; mocks.partner.getAll.mockResolvedValue([partner]); mocks.map.getMapMarkers.mockResolvedValue([marker]); @@ -61,21 +67,24 @@ describe(MapService.name, () => { }); it('should include assets from shared albums', async () => { - const asset = assetStub.withLocation; + const auth = AuthFactory.create(userStub.user1); + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); const marker = { id: asset.id, - lat: asset.exifInfo!.latitude!, - lon: asset.exifInfo!.longitude!, - city: asset.exifInfo!.city, - state: asset.exifInfo!.state, - country: asset.exifInfo!.country, + lat: asset.exifInfo.latitude!, + lon: asset.exifInfo.longitude!, + city: asset.exifInfo.city, + state: asset.exifInfo.state, + country: asset.exifInfo.country, }; mocks.partner.getAll.mockResolvedValue([]); mocks.map.getMapMarkers.mockResolvedValue([marker]); - mocks.album.getOwned.mockResolvedValue([albumStub.empty]); - mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]); + mocks.album.getOwned.mockResolvedValue([AlbumFactory.create()]); + mocks.album.getShared.mockResolvedValue([AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()]); - const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true }); + const markers = await sut.getMapMarkers(auth, { withSharedAlbums: true }); expect(markers).toHaveLength(1); expect(markers[0]).toEqual(marker); diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 823383c29d..bf2cbc62fa 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,10 +1,13 @@ import { OutputInfo } from 'sharp'; import { SystemConfig } from 'src/config'; import { Exif } from 'src/database'; +import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, AssetPathType, + AssetStatus, AssetType, + AssetVisibility, AudioCodec, Colorspace, ExifOrientation, @@ -19,7 +22,6 @@ import { import { MediaService } from 'src/services/media.service'; import { JobCounts, RawImageInfo } from 'src/types'; import { AssetFactory } from 'test/factories/asset.factory'; -import { assetStub, previewFile } from 'test/fixtures/asset.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { probeStub } from 'test/fixtures/media.stub'; import { personStub, personThumbnailStub } from 'test/fixtures/person.stub'; @@ -43,9 +45,12 @@ describe(MediaService.name, () => { expect(sut).toBeDefined(); }); + // TODO these should all become medium tests of either the service or the repository. + // The entire logic of what to queue lives in the SQL query now describe('handleQueueGenerateThumbnails', () => { it('should queue all assets', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream([personStub.newThumbnail])); @@ -55,7 +60,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); @@ -69,7 +74,8 @@ describe(MediaService.name, () => { }); it('should queue trashed assets when force is true', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.archived])); + const asset = AssetFactory.create({ status: AssetStatus.Trashed, deletedAt: new Date() }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: true }); @@ -78,13 +84,14 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.trashed.id }, + data: { id: asset.id }, }, ]); }); it('should queue archived assets when force is true', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.archived])); + const asset = AssetFactory.create({ visibility: AssetVisibility.Archive }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: true }); @@ -93,13 +100,13 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.archived.id }, + data: { id: asset.id }, }, ]); }); it('should queue all people with missing thumbnail path', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image])); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([AssetFactory.create()])); mocks.person.getAll.mockReturnValue(makeStream([personStub.noThumbnail, personStub.noThumbnail])); mocks.person.getRandomFace.mockResolvedValueOnce(faceStub.face1); @@ -120,7 +127,8 @@ describe(MediaService.name, () => { }); it('should queue all assets with missing resize path', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noResizePath])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: false }); @@ -128,7 +136,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); @@ -196,7 +204,8 @@ describe(MediaService.name, () => { }); it('should queue assets with edits but missing edited thumbnails', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit])); + const asset = AssetFactory.from().edit().build(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: false }); @@ -204,7 +213,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetEditThumbnailGeneration, - data: { id: assetStub.withCropEdit.id }, + data: { id: asset.id }, }, ]); @@ -212,8 +221,9 @@ describe(MediaService.name, () => { }); it('should not queue assets with missing edited fullsize when feature is disabled', async () => { + const asset = AssetFactory.from().edit().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } }); - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit])); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: false }); @@ -242,7 +252,8 @@ describe(MediaService.name, () => { }); it('should queue both regular and edited thumbnails for assets with edits when force is true', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit])); + const asset = AssetFactory.from().edit().build(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: true }); @@ -250,11 +261,11 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.withCropEdit.id }, + data: { id: asset.id }, }, { name: JobName.AssetEditThumbnailGeneration, - data: { id: assetStub.withCropEdit.id }, + data: { id: asset.id }, }, ]); @@ -264,16 +275,15 @@ describe(MediaService.name, () => { describe('handleQueueMigration', () => { it('should remove empty directories and queue jobs', async () => { - mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([asset])); mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts); mocks.person.getAll.mockReturnValue(makeStream([personStub.withName])); await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.Success); expect(mocks.storage.removeEmptyDirs).toHaveBeenCalledTimes(2); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.AssetFileMigration, data: { id: assetStub.image.id } }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetFileMigration, data: { id: asset.id } }]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonFileMigration, data: { id: personStub.withName.id } }, ]); @@ -283,39 +293,42 @@ describe(MediaService.name, () => { describe('handleAssetMigration', () => { it('should fail if asset does not exist', async () => { mocks.assetJob.getForMigrationJob.mockResolvedValue(void 0); - await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed); + await expect(sut.handleAssetMigration({ id: 'non-existent' })).resolves.toBe(JobStatus.Failed); expect(mocks.move.getByEntity).not.toHaveBeenCalled(); }); it('should move asset files', async () => { - mocks.assetJob.getForMigrationJob.mockResolvedValue(assetStub.image); + const asset = AssetFactory.from() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .build(); + mocks.assetJob.getForMigrationJob.mockResolvedValue(asset); mocks.move.create.mockResolvedValue({ - entityId: assetStub.image.id, + entityId: asset.id, id: 'move-id', newPath: '/new/path', oldPath: '/old/path', pathType: AssetPathType.Original, }); - await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success); + await expect(sut.handleAssetMigration({ id: asset.id })).resolves.toBe(JobStatus.Success); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: assetStub.image.id, + entityId: asset.id, pathType: AssetFileType.FullSize, - oldPath: '/uploads/user-id/fullsize/path.webp', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_fullsize.jpeg'), + oldPath: asset.files[0].path, + newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_fullsize.jpeg`, }); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: assetStub.image.id, + entityId: asset.id, pathType: AssetFileType.Preview, - oldPath: '/uploads/user-id/thumbs/path.jpg', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_preview.jpeg'), + oldPath: asset.files[1].path, + newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`, }); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: assetStub.image.id, + entityId: asset.id, pathType: AssetFileType.Thumbnail, - oldPath: '/uploads/user-id/webp/path.ext', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_thumbnail.webp'), + oldPath: asset.files[2].path, + newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.webp`, }); expect(mocks.move.create).toHaveBeenCalledTimes(3); }); @@ -339,66 +352,71 @@ describe(MediaService.name, () => { it('should skip thumbnail generation if asset not found', async () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(void 0); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: 'non-existent' }); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should skip thumbnail generation if asset type is unknown', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ ...assetStub.image, type: 'foo' as AssetType }); + const asset = AssetFactory.create({ type: 'foo' as AssetType }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await expect(sut.handleGenerateThumbnails({ id: assetStub.image.id })).resolves.toBe(JobStatus.Skipped); + await expect(sut.handleGenerateThumbnails({ id: asset.id })).resolves.toBe(JobStatus.Skipped); expect(mocks.media.probe).not.toHaveBeenCalled(); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should skip video thumbnail generation if no video stream', async () => { + const asset = AssetFactory.create({ type: AssetType.Video }); mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await expect(sut.handleGenerateThumbnails({ id: assetStub.video.id })).rejects.toThrowError(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await expect(sut.handleGenerateThumbnails({ id: asset.id })).rejects.toThrowError(); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should skip invisible assets', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.livePhotoMotionAsset); + const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - expect(await sut.handleGenerateThumbnails({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); + expect(await sut.handleGenerateThumbnails({ id: asset.id })).toEqual(JobStatus.Skipped); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should delete previous preview if different path', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FileDelete, data: { - files: expect.arrayContaining([previewFile.path]), + files: expect.arrayContaining([asset.files[0].path]), }, }); }); it('should generate P3 thumbnails for a wide gamut image', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ - ...assetStub.image, - exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as Exif, - }); + const asset = AssetFactory.from() + .exif({ profileDescription: 'Adobe RGB', bitsPerSample: 14 }) + .files([AssetFileType.Preview, AssetFileType.Thumbnail]) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, @@ -444,27 +462,28 @@ describe(MediaService.name, () => { expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), isEdited: false, isProgressive: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), isEdited: false, isProgressive: false, }, ]); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, thumbhash: thumbhashBuffer }); }); it('should generate a thumbnail for a video', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( @@ -484,14 +503,14 @@ describe(MediaService.name, () => { ); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), isEdited: false, isProgressive: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), isEdited: false, @@ -501,9 +520,10 @@ describe(MediaService.name, () => { }); it('should tonemap thumbnail for hdr video', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( @@ -523,14 +543,14 @@ describe(MediaService.name, () => { ); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), isEdited: false, isProgressive: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), isEdited: false, @@ -540,12 +560,13 @@ describe(MediaService.name, () => { }); it('should always generate video thumbnail in one pass', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '5000k' }, }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -565,9 +586,10 @@ describe(MediaService.name, () => { }); it('should not skip intra frames for MTS file', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamMTS); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -582,9 +604,10 @@ describe(MediaService.name, () => { }); it('should override reserved color metadata', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamReserved); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -601,10 +624,11 @@ describe(MediaService.name, () => { }); it('should use scaling divisible by 2 even when using quick sync', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -618,18 +642,19 @@ describe(MediaService.name, () => { }); it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { format } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = `/data/thumbs/user-id/as/se/asset-id_preview.${format}`; - const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id_thumbnail.webp`; + const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.${format}`; + const thumbnailPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.webp`; - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, @@ -667,18 +692,19 @@ describe(MediaService.name, () => { }); it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_preview.jpeg`); - const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_thumbnail.${format}`); + const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`; + const thumbnailPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.${format}`; - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, @@ -716,12 +742,13 @@ describe(MediaService.name, () => { }); it('should generate progressive JPEG for preview when enabled', async () => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { progressive: true }, thumbnail: { progressive: false } }, }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, @@ -752,12 +779,13 @@ describe(MediaService.name, () => { }); it('should generate progressive JPEG for thumbnail when enabled', async () => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } }, }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, @@ -788,13 +816,14 @@ describe(MediaService.name, () => { }); it('should never set isProgressive for videos', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { progressive: true }, thumbnail: { progressive: true } }, }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ expect.objectContaining({ @@ -809,26 +838,30 @@ describe(MediaService.name, () => { }); it('should delete previous thumbnail if different path', async () => { + const asset = AssetFactory.from().exif().file({ type: AssetFileType.Preview }).build(); mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FileDelete, data: { - files: expect.arrayContaining([previewFile.path]), + files: expect.arrayContaining([asset.files[0].path]), }, }); }); it('should extract embedded image if enabled and available', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, { @@ -839,14 +872,17 @@ describe(MediaService.name, () => { }); it('should resize original image if embedded image is too small', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, @@ -854,13 +890,16 @@ describe(MediaService.name, () => { }); it('should resize original image if embedded image not found', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, @@ -868,14 +907,17 @@ describe(MediaService.name, () => { }); it('should resize original image if embedded image extraction is not enabled', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.extract).not.toHaveBeenCalled(); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, @@ -884,14 +926,17 @@ describe(MediaService.name, () => { it('should process invalid images if enabled', async () => { vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true'); + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith( - assetStub.imageDng.originalPath, + asset.originalPath, expect.objectContaining({ processInvalidImages: true }), ); @@ -917,14 +962,18 @@ describe(MediaService.name, () => { }); it('should extract full-size JPEG preview from RAW', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, { @@ -951,14 +1000,18 @@ describe(MediaService.name, () => { }); it('should convert full-size WEBP preview from JXL preview of RAW', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, { @@ -997,15 +1050,19 @@ describe(MediaService.name, () => { }); it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, }); @@ -1079,15 +1136,16 @@ describe(MediaService.name, () => { }); it('should skip generating full-size preview for web-friendly images', async () => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, @@ -1116,7 +1174,7 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { @@ -1161,7 +1219,7 @@ describe(MediaService.name, () => { .build(); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { @@ -1231,22 +1289,31 @@ describe(MediaService.name, () => { }); it('should skip videos', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); + const asset = AssetFactory.from({ type: AssetType.Video }).exif().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await expect(sut.handleAssetEditThumbnailGeneration({ id: assetStub.video.id })).resolves.toBe(JobStatus.Success); + await expect(sut.handleAssetEditThumbnailGeneration({ id: asset.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); }); it('should upsert 3 edited files for edit jobs', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ - ...assetStub.withCropEdit, - }); + const asset = AssetFactory.from() + .exif() + .edit({ action: AssetEditAction.Crop }) + .files([ + { type: AssetFileType.FullSize, isEdited: true }, + { type: AssetFileType.Preview, isEdited: true }, + { type: AssetFileType.Thumbnail, isEdited: true }, + ]) + .build(); + + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); mocks.person.getFaces.mockResolvedValue([]); mocks.ocr.getByAssetId.mockResolvedValue([]); - await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id }); + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith( expect.arrayContaining([ @@ -1258,21 +1325,23 @@ describe(MediaService.name, () => { }); it('should apply edits when generating thumbnails', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ - ...assetStub.withCropEdit, - }); + const asset = AssetFactory.from() + .exif() + .edit({ action: AssetEditAction.Crop, parameters: { height: 1152, width: 1512, x: 216, y: 1512 } }) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); mocks.person.getFaces.mockResolvedValue([]); mocks.ocr.getByAssetId.mockResolvedValue([]); - await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id }); + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( rawBuffer, expect.objectContaining({ edits: [ - { + expect.objectContaining({ action: 'crop', parameters: { height: 1152, width: 1512, x: 216, y: 1512 }, - }, + }), ], }), expect.any(String), @@ -1305,13 +1374,12 @@ describe(MediaService.name, () => { }); it('should generate all 3 edited files if an asset has edits', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ - ...assetStub.withCropEdit, - }); + const asset = AssetFactory.from().exif().edit().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); mocks.person.getFaces.mockResolvedValue([]); mocks.ocr.getByAssetId.mockResolvedValue([]); - await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id }); + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( @@ -1336,21 +1404,20 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); mocks.media.generateThumbhash.mockResolvedValue(factory.buffer()); - await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id, source: 'upload' }); + await sut.handleAssetEditThumbnailGeneration({ id: asset.id, source: 'upload' }); expect(mocks.media.generateThumbhash).toHaveBeenCalled(); }); it('should apply thumbhash if job source is edit and edits exist', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ - ...assetStub.withCropEdit, - }); + const asset = AssetFactory.from().exif().edit().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = factory.buffer(); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); mocks.person.getFaces.mockResolvedValue([]); mocks.ocr.getByAssetId.mockResolvedValue([]); - await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id }); + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ thumbhash: thumbhashBuffer })); }); @@ -1439,7 +1506,7 @@ describe(MediaService.name, () => { expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.previewPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(expect.any(String), { colorspace: Colorspace.P3, orientation: undefined, processInvalidImages: false, @@ -1753,7 +1820,8 @@ describe(MediaService.name, () => { describe('handleQueueVideoConversion', () => { it('should queue all video assets', async () => { - mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video])); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueVideoConversion({ force: true }); @@ -1762,13 +1830,14 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetEncodeVideo, - data: { id: assetStub.video.id }, + data: { id: asset.id }, }, ]); }); it('should queue all video assets without encoded videos', async () => { - mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video])); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([asset])); await sut.handleQueueVideoConversion({}); @@ -1776,7 +1845,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetEncodeVideo, - data: { id: assetStub.video.id }, + data: { id: asset.id }, }, ]); }); @@ -1784,13 +1853,14 @@ describe(MediaService.name, () => { describe('handleVideoConversion', () => { beforeEach(() => { - mocks.assetJob.getForVideoConversion.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ id: 'video-id', type: AssetType.Video, originalPath: '/original/path.ext' }); + mocks.assetJob.getForVideoConversion.mockResolvedValue(asset); sut.videoInterfaces = { dri: ['renderD128'], mali: true }; }); it('should skip transcoding if asset not found', async () => { mocks.assetJob.getForVideoConversion.mockResolvedValue(void 0); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).not.toHaveBeenCalled(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1799,7 +1869,7 @@ describe(MediaService.name, () => { mocks.logger.isLevelEnabled.mockReturnValue(false); mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -1819,7 +1889,7 @@ describe(MediaService.name, () => { mocks.logger.isLevelEnabled.mockReturnValue(false); mocks.media.probe.mockResolvedValue(probeStub.multipleAudioStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -1837,13 +1907,13 @@ describe(MediaService.name, () => { it('should skip a video without any streams', async () => { mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should skip a video without any height', async () => { mocks.media.probe.mockResolvedValue(probeStub.noHeight); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1851,7 +1921,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.noAudioStreams); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'foo' } } as never as SystemConfig); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1862,14 +1932,14 @@ describe(MediaService.name, () => { }); mocks.media.transcode.mockRejectedValue(new Error('Error transcoding video')); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.Failed); + await expect(sut.handleVideoConversion({ id: 'video-id' })).resolves.toBe(JobStatus.Failed); expect(mocks.media.transcode).toHaveBeenCalledTimes(1); }); it('should transcode when set to all', async () => { mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1884,7 +1954,7 @@ describe(MediaService.name, () => { it('should transcode when optimal and too big', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1899,7 +1969,7 @@ describe(MediaService.name, () => { it('should transcode when policy bitrate and bitrate higher than max bitrate', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: '30M' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1914,7 +1984,7 @@ describe(MediaService.name, () => { it('should transcode when max bitrate is not a number', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: 'foo' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1931,7 +2001,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1946,7 +2016,7 @@ describe(MediaService.name, () => { it('should scale horizontally when video is horizontal', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1961,7 +2031,7 @@ describe(MediaService.name, () => { it('should scale vertically when video is vertical', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1978,7 +2048,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1995,7 +2065,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2012,7 +2082,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Hevc, acceptedAudioCodecs: [AudioCodec.Aac] }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2033,7 +2103,7 @@ describe(MediaService.name, () => { acceptedAudioCodecs: [AudioCodec.Aac], }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2054,7 +2124,7 @@ describe(MediaService.name, () => { acceptedAudioCodecs: [AudioCodec.Aac], }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2069,7 +2139,7 @@ describe(MediaService.name, () => { it('should copy audio stream when audio matches target', async () => { mocks.media.probe.mockResolvedValue(probeStub.audioStreamAac); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2083,7 +2153,7 @@ describe(MediaService.name, () => { it('should remux when input is not an accepted container', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamAvi); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2099,33 +2169,33 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'invalid' as any } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not transcode if transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not remux when input is not an accepted container and transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not transcode if target codec is invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: 'invalid' as any } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should delete existing transcode if current policy does not require transcoding', async () => { - const asset = assetStub.hasEncodedVideo; + const asset = AssetFactory.create({ type: AssetType.Video, encodedVideoPath: '/encoded/video/path.mp4' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); mocks.assetJob.getForVideoConversion.mockResolvedValue(asset); @@ -2142,7 +2212,7 @@ describe(MediaService.name, () => { it('should set max bitrate if above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500k' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2157,7 +2227,7 @@ describe(MediaService.name, () => { it('should default max bitrate to kbps if no unit is provided', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2172,7 +2242,7 @@ describe(MediaService.name, () => { it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2187,7 +2257,7 @@ describe(MediaService.name, () => { it('should fallback to one pass for h264/h265 if two-pass is enabled but no max bitrate is set', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2208,7 +2278,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Vp9, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2229,7 +2299,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Vp9, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2244,7 +2314,7 @@ describe(MediaService.name, () => { it('should configure preset for vp9', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, preset: 'slow' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2259,7 +2329,7 @@ describe(MediaService.name, () => { it('should not configure preset for vp9 if invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.Vp9 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2274,7 +2344,7 @@ describe(MediaService.name, () => { it('should configure threads if above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, threads: 2 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2289,7 +2359,7 @@ describe(MediaService.name, () => { it('should disable thread pooling for h264 if thread limit is 1', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2304,7 +2374,7 @@ describe(MediaService.name, () => { it('should omit thread flags for h264 if thread limit is at or below 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2319,7 +2389,7 @@ describe(MediaService.name, () => { it('should disable thread pooling for hevc if thread limit is 1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.Hevc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2334,7 +2404,7 @@ describe(MediaService.name, () => { it('should omit thread flags for hevc if thread limit is at or below 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.Hevc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2349,7 +2419,7 @@ describe(MediaService.name, () => { it('should use av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2374,7 +2444,7 @@ describe(MediaService.name, () => { it('should map `veryslow` preset to 4 for av1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, preset: 'veryslow' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2389,7 +2459,7 @@ describe(MediaService.name, () => { it('should set max bitrate for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, maxBitrate: '2M' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2404,7 +2474,7 @@ describe(MediaService.name, () => { it('should set threads for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2421,7 +2491,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4, maxBitrate: '2M' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2442,7 +2512,7 @@ describe(MediaService.name, () => { targetResolution: '1080p', }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -2451,21 +2521,21 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, targetVideoCodec: VideoCodec.Vp9 }, }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should fail if hwaccel option is invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should set options for nvenc', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2502,7 +2572,7 @@ describe(MediaService.name, () => { twoPass: true, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2519,7 +2589,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2536,7 +2606,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2553,7 +2623,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2568,7 +2638,7 @@ describe(MediaService.name, () => { it('should ignore two pass for nvenc if max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2585,7 +2655,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2607,7 +2677,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2628,7 +2698,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2645,7 +2715,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2685,7 +2755,7 @@ describe(MediaService.name, () => { preferredHwDevice: '/dev/dri/renderD128', }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2705,7 +2775,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2725,7 +2795,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, targetVideoCodec: VideoCodec.Vp9 }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2745,7 +2815,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -2754,7 +2824,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2775,7 +2845,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2801,7 +2871,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2830,7 +2900,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true, preferredHwDevice: 'renderD129' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2848,7 +2918,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2869,7 +2939,7 @@ describe(MediaService.name, () => { it('should set options for vaapi', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2901,7 +2971,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2925,7 +2995,7 @@ describe(MediaService.name, () => { it('should set cq options for vaapi when max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2951,7 +3021,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2970,7 +3040,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2991,7 +3061,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preferredHwDevice: '/dev/dri/renderD128' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3012,7 +3082,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -3037,7 +3107,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -3060,7 +3130,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -3080,7 +3150,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true, preferredHwDevice: 'renderD129' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3098,7 +3168,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -3121,7 +3191,7 @@ describe(MediaService.name, () => { }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(3); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -3138,7 +3208,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -3155,7 +3225,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: [], mali: true }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -3164,7 +3234,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3204,7 +3274,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Hevc, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3221,7 +3291,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3238,7 +3308,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3260,7 +3330,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3279,7 +3349,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: false, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3301,7 +3371,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3320,7 +3390,7 @@ describe(MediaService.name, () => { it('should tonemap when policy is required and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3339,7 +3409,7 @@ describe(MediaService.name, () => { it('should tonemap when policy is optimal and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3358,7 +3428,7 @@ describe(MediaService.name, () => { it('should transcode when policy is required and video is not yuv420p', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3373,7 +3443,7 @@ describe(MediaService.name, () => { it('should convert to yuv420p when scaling without tone-mapping', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream4K10Bit); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -3389,10 +3459,10 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.logger.isLevelEnabled.mockReturnValue(true); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: true }); - expect(mocks.media.transcode).toHaveBeenCalledWith(assetStub.video.originalPath, expect.any(String), { + expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: true }); + expect(mocks.media.transcode).toHaveBeenCalledWith('/original/path.ext', expect.any(String), { inputOptions: expect.any(Array), outputOptions: expect.any(Array), twoPass: false, @@ -3406,19 +3476,23 @@ describe(MediaService.name, () => { it('should not count frames for progress when log level is not debug', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.logger.isLevelEnabled.mockReturnValue(false); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false }); + expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); }); it('should process unknown audio stream', async () => { + const asset = AssetFactory.create({ + type: AssetType.Video, + originalPath: '/original/path.ext', + }); mocks.media.probe.mockResolvedValue(probeStub.audioStreamUnknown); - mocks.asset.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleVideoConversion({ id: assetStub.video.id }); + mocks.asset.getByIds.mockResolvedValue([asset]); + await sut.handleVideoConversion({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( - '/original/path.ext', - '/data/encoded-video/user-id/as/se/asset-id.mp4', + asset.originalPath, + expect.stringContaining('video-id.mp4'), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:a copy']), diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index d94de020e0..8530f6fed2 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -3,7 +3,6 @@ import { DateTime } from 'luxon'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { defaults } from 'src/config'; -import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetFileType, AssetType, @@ -17,8 +16,6 @@ import { import { ImmichTags } from 'src/repositories/metadata.repository'; import { firstDateTime, MetadataService } from 'src/services/metadata.service'; import { AssetFactory } from 'test/factories/asset.factory'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; import { personStub } from 'test/fixtures/person.stub'; import { tagStub } from 'test/fixtures/tag.stub'; @@ -125,27 +122,29 @@ describe(MetadataService.name, () => { describe('handleQueueMetadataExtraction', () => { it('should queue metadata extraction for all assets without exif values', async () => { - mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([asset])); await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetExtractMetadata, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should queue metadata extraction for all assets', async () => { - mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([asset])); await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetExtractMetadata, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); @@ -166,9 +165,9 @@ describe(MetadataService.name, () => { it('should handle an asset that could not be found', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(void 0); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: 'non-existent' }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith('non-existent'); expect(mocks.asset.upsertExif).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -287,8 +286,8 @@ describe(MetadataService.name, () => { } as Stats); mockReadTags({ ISO: [160] }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }), { lockedPropertiesBehavior: 'skip', }); @@ -406,7 +405,7 @@ describe(MetadataService.name, () => { mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { userId: asset.ownerId, @@ -546,68 +545,68 @@ describe(MetadataService.name, () => { mockReadTags({ HierarchicalSubject: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined }); expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: '2024', parent: undefined }); }); it('should extract ignore / characters in a HierarchicalSubject tag', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Mom|Dad'] }) }); mockReadTags({ HierarchicalSubject: ['Mom/Dad'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ - userId: 'user-id', + userId: asset.ownerId, value: 'Mom|Dad', parent: undefined, }); }); it('should ignore HierarchicalSubject when TagsList is present', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); - mocks.asset.getById.mockResolvedValue({ - ...factory.asset(), - exifInfo: factory.exif({ tags: ['Parent/Child', 'Parent2/Child2'] }), - }); + const baseAsset = AssetFactory.from(); + const asset = baseAsset.build(); + const updatedAsset = baseAsset.exif({ tags: ['Parent/Child', 'Parent2/Child2'] }).build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getById.mockResolvedValue(updatedAsset); mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent', parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent/Child', parentId: 'tag-parent', }); }); it('should remove existing tags', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({}); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.tag.replaceAssetTags).toHaveBeenCalledWith('asset-id', []); + expect(mocks.tag.replaceAssetTags).toHaveBeenCalledWith(asset.id, []); }); it('should not apply motion photos if asset is video', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); @@ -617,23 +616,25 @@ describe(MetadataService.name, () => { }); it('should handle an invalid Directory Item', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ MotionPhoto: 1, ContainerDirectory: [{ Foo: 100 }], }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); }); it('should extract the correct video orientation', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); mockReadTags({}); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }), { lockedPropertiesBehavior: 'skip' }, @@ -641,16 +642,14 @@ describe(MetadataService.name, () => { }); it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); mockReadTags({ Directory: 'foo/bar/', @@ -662,57 +661,52 @@ describe(MetadataService.name, () => { EmbeddedVideoType: 'MotionPhoto_Data', }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.metadata.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - 'MotionPhotoVideo', - ); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'MotionPhotoVideo'); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => { + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', EmbeddedVideoFile: new BinaryField(0, ''), @@ -720,56 +714,51 @@ describe(MetadataService.name, () => { MotionPhoto: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.metadata.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - 'EmbeddedVideoFile', - ); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'EmbeddedVideoFile'); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should extract the motion photo video from the XMP directory entry ', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); mockReadTags({ Directory: 'foo/bar/', @@ -778,47 +767,46 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); - expect(mocks.storage.readFile).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - expect.any(Object), - ); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object)); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should delete old motion photo video assets if they do not match what is extracted', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoWithOriginalFileName); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -826,21 +814,21 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockImplementation( - (asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise, - ); + mocks.asset.create.mockResolvedValue(AssetFactory.create({ type: AssetType.Video })); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.job.queue).toHaveBeenNthCalledWith(1, { name: JobName.AssetDelete, - data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId, deleteOnDisk: true }, + data: { id: asset.livePhotoVideoId, deleteOnDisk: true }, }); }); it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -848,12 +836,12 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.asset.getByChecksum.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); mocks.storage.checkFileExists.mockResolvedValue(true); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.create).not.toHaveBeenCalled(); expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); // The still asset gets saved by handleMetadataExtraction, but not the video @@ -862,10 +850,9 @@ describe(MetadataService.name, () => { }); it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - livePhotoVideoId: null, - }); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -873,31 +860,26 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.getByChecksum.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); + mocks.asset.getByChecksum.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(4); }); it('should not update storage usage if motion photo is external', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - livePhotoVideoId: null, - isExternal: true, - }); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ isExternal: true }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -905,16 +887,17 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.asset.create.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.user.updateUsage).not.toHaveBeenCalled(); }); it('should save all metadata', async () => { const dateForTest = new Date('1970-01-01T00:00:00.000-11:30'); + const asset = AssetFactory.create(); const tags: ImmichTags = { BitsPerSample: 1, @@ -941,14 +924,14 @@ describe(MetadataService.name, () => { Rating: 3, }; - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(tags); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( { - assetId: assetStub.image.id, + assetId: asset.id, bitsPerSample: expect.any(Number), autoStackId: null, colorspace: tags.ColorSpace, @@ -983,7 +966,7 @@ describe(MetadataService.name, () => { ); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: null, fileCreatedAt: dateForTest, localDateTime: DateTime.fromISO('1970-01-01T00:00:00.000Z').toJSDate(), @@ -996,6 +979,7 @@ describe(MetadataService.name, () => { // https://github.com/photostructure/exiftool-vendored.js/issues/203 // this only tests our assumptions of exiftool-vendored, demonstrating the issue + const asset = AssetFactory.create(); const someDate = '2024-09-01T00:00:00.000'; expect(ExifDateTime.fromISO(someDate + 'Z')?.zone).toBe('UTC'); expect(ExifDateTime.fromISO(someDate + '+00:00')?.zone).toBe('UTC'); // this is the issue, should be UTC+0 @@ -1005,11 +989,11 @@ describe(MetadataService.name, () => { DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'), tz: undefined, }; - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(tags); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ timeZone: 'UTC+0', @@ -1019,7 +1003,8 @@ describe(MetadataService.name, () => { }); it('should extract duration', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1028,20 +1013,21 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: '00:00:06.210', }), ); }); it('should only extract duration for videos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1049,20 +1035,21 @@ describe(MetadataService.name, () => { duration: 6.21, }, }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: null, }), ); }); it('should omit duration of zero', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1071,20 +1058,21 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: null, }), ); }); it('should a handle duration of 1 week', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1093,65 +1081,55 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.video.id, + id: asset.id, duration: '168:00:00.000', }), ); }); it('should use Duration from exif', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.image, - originalPath: '/original/path.webp', - }); + const asset = AssetFactory.create({ originalFileName: 'file.webp' }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, {}); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' })); }); it('should prefer Duration from exif over sidecar', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.image, - originalPath: '/original/path.webp', - files: [ - { - id: 'some-id', - type: AssetFileType.Sidecar, - path: '/path/to/something', - isEdited: false, - }, - ], - }); + const asset = AssetFactory.from({ originalFileName: 'file.webp' }).file({ type: AssetFileType.Sidecar }).build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, { Duration: 456 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' })); }); it('should ignore all Duration tags for definitely static images', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.imageDng); + const asset = AssetFactory.from({ originalFileName: 'file.dng' }).build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, { Duration: 456 }); - await sut.handleMetadataExtraction({ id: assetStub.imageDng.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null })); }); it('should ignore Duration from exif for videos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, {}); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, @@ -1161,17 +1139,18 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' })); }); it('should trim whitespace from description', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Description: '\t \v \f \n \r' }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ description: '', @@ -1180,7 +1159,7 @@ describe(MetadataService.name, () => { ); mockReadTags({ ImageDescription: ' my\n description' }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ description: 'my\n description', @@ -1190,10 +1169,11 @@ describe(MetadataService.name, () => { }); it('should handle a numeric description', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Description: 1000 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ description: '1000', @@ -1203,55 +1183,60 @@ describe(MetadataService.name, () => { }); it('should skip importing metadata when the feature is disabled', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } }); mockReadTags(makeFaceTags({ Name: 'Person 1' })); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.getDistinctNames).not.toHaveBeenCalled(); }); it('should skip importing metadata face for assets without tags.RegionInfo', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.getDistinctNames).not.toHaveBeenCalled(); }); it('should skip importing faces without name', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags()); mocks.person.getDistinctNames.mockResolvedValue([]); mocks.person.createAll.mockResolvedValue([]); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.createAll).not.toHaveBeenCalled(); expect(mocks.person.refreshFaces).not.toHaveBeenCalled(); expect(mocks.person.updateAll).not.toHaveBeenCalled(); }); it('should skip importing faces with empty name', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: '' })); mocks.person.getDistinctNames.mockResolvedValue([]); mocks.person.createAll.mockResolvedValue([]); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.createAll).not.toHaveBeenCalled(); expect(mocks.person.refreshFaces).not.toHaveBeenCalled(); expect(mocks.person.updateAll).not.toHaveBeenCalled(); }); - it('should apply metadata face tags creating new persons', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + it('should apply metadata face tags creating new people', async () => { + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: personStub.withName.name })); mocks.person.getDistinctNames.mockResolvedValue([]); mocks.person.createAll.mockResolvedValue([personStub.withName.id]); mocks.person.update.mockResolvedValue(personStub.withName); - await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id); - expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true }); expect(mocks.person.createAll).toHaveBeenCalledWith([ expect.objectContaining({ name: personStub.withName.name }), ]); @@ -1259,7 +1244,7 @@ describe(MetadataService.name, () => { [ { id: 'random-uuid', - assetId: assetStub.primaryImage.id, + assetId: asset.id, personId: 'random-uuid', imageHeight: 100, imageWidth: 1000, @@ -1273,7 +1258,7 @@ describe(MetadataService.name, () => { [], ); expect(mocks.person.updateAll).toHaveBeenCalledWith([ - { id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' }, + { id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' }, ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { @@ -1284,21 +1269,22 @@ describe(MetadataService.name, () => { }); it('should assign metadata face tags to existing persons', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: personStub.withName.name })); mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]); mocks.person.createAll.mockResolvedValue([]); mocks.person.update.mockResolvedValue(personStub.withName); - await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id); - expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true }); expect(mocks.person.createAll).not.toHaveBeenCalled(); expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [ { id: 'random-uuid', - assetId: assetStub.primaryImage.id, + assetId: asset.id, personId: personStub.withName.id, imageHeight: 100, imageWidth: 1000, @@ -1368,16 +1354,17 @@ describe(MetadataService.name, () => { 'should transform RegionInfo geometry according to exif orientation $description', async ({ orientation, expected }) => { const { imgW, imgH, x1, x2, y1, y2 } = expected; + const asset = AssetFactory.create(); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: personStub.withName.name }, orientation)); mocks.person.getDistinctNames.mockResolvedValue([]); mocks.person.createAll.mockResolvedValue([personStub.withName.id]); mocks.person.update.mockResolvedValue(personStub.withName); - await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id); - expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true, }); expect(mocks.person.createAll).toHaveBeenCalledWith([ @@ -1387,7 +1374,7 @@ describe(MetadataService.name, () => { [ { id: 'random-uuid', - assetId: assetStub.primaryImage.id, + assetId: asset.id, personId: 'random-uuid', imageWidth: imgW, imageHeight: imgH, @@ -1401,7 +1388,7 @@ describe(MetadataService.name, () => { [], ); expect(mocks.person.updateAll).toHaveBeenCalledWith([ - { id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' }, + { id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' }, ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { @@ -1414,10 +1401,11 @@ describe(MetadataService.name, () => { }); it('should handle invalid modify date', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ModifyDate: '00:00:00.000' }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ modifyDate: expect.any(Date), @@ -1427,10 +1415,11 @@ describe(MetadataService.name, () => { }); it('should handle invalid rating value', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Rating: 6 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ rating: null, @@ -1440,10 +1429,11 @@ describe(MetadataService.name, () => { }); it('should handle valid rating value', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Rating: 5 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ rating: 5, @@ -1453,10 +1443,11 @@ describe(MetadataService.name, () => { }); it('should handle valid negative rating value', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Rating: -1 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ rating: -1, @@ -1466,11 +1457,12 @@ describe(MetadataService.name, () => { }); it('should handle livePhotoCID not set', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith( expect.objectContaining({ visibility: AssetVisibility.Hidden }), @@ -1479,17 +1471,18 @@ describe(MetadataService.name, () => { }); it('should handle not finding a match', async () => { + const asset = AssetFactory.create({ type: AssetType.Video }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ livePhotoCID: 'CID', - ownerId: assetStub.livePhotoMotionAsset.ownerId, - otherAssetId: assetStub.livePhotoMotionAsset.id, + ownerId: asset.ownerId, + otherAssetId: asset.id, libraryId: null, type: AssetType.Image, }); @@ -1500,65 +1493,67 @@ describe(MetadataService.name, () => { }); it('should link photo and video', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ + libraryId: null, livePhotoCID: 'CID', - ownerId: assetStub.livePhotoStillAsset.ownerId, - otherAssetId: assetStub.livePhotoStillAsset.id, + ownerId: asset.ownerId, + otherAssetId: asset.id, type: AssetType.Video, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); - expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); + expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([motionAsset.id]); }); it('should notify clients on live photo link', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - }); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { - userId: assetStub.livePhotoMotionAsset.ownerId, - assetId: assetStub.livePhotoMotionAsset.id, + userId: motionAsset.ownerId, + assetId: motionAsset.id, }); }); it('should search by libraryId', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - libraryId: 'library-id', - }); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' }); + const asset = AssetFactory.create({ libraryId: 'library-id' }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetMetadataExtracted', { - assetId: assetStub.livePhotoStillAsset.id, - userId: assetStub.livePhotoStillAsset.ownerId, + assetId: asset.id, + userId: asset.ownerId, }); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ - ownerId: 'user-id', - otherAssetId: 'live-photo-still-asset', + ownerId: asset.ownerId, + otherAssetId: asset.id, livePhotoCID: 'CID', libraryId: 'library-id', - type: 'VIDEO', + type: AssetType.Video, }); }); @@ -1579,10 +1574,11 @@ describe(MetadataService.name, () => { }, { exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } }, ])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(exif); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining(expected), { lockedPropertiesBehavior: 'skip', }); @@ -1603,10 +1599,11 @@ describe(MetadataService.name, () => { { exif: { LensID: ' Unknown 6-30mm' }, expected: null }, { exif: { LensID: '' }, expected: null }, ])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(exif); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ lensModel: expected, @@ -1616,10 +1613,11 @@ describe(MetadataService.name, () => { }); it('should properly set width/height for normal images', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ width: 1000, @@ -1629,10 +1627,11 @@ describe(MetadataService.name, () => { }); it('should properly swap asset width/height for rotated images', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ width: 2000, @@ -1642,14 +1641,11 @@ describe(MetadataService.name, () => { }); it('should not overwrite existing width/height if they already exist', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.image, - width: 1920, - height: 1080, - }); + const asset = AssetFactory.create({ width: 1920, height: 1080 }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ImageWidth: 1280, ImageHeight: 720 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.update).not.toHaveBeenCalledWith( expect.objectContaining({ width: 1280, @@ -1685,7 +1681,7 @@ describe(MetadataService.name, () => { it('should do nothing if asset could not be found', async () => { mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(void 0); - await expect(sut.handleSidecarCheck({ id: assetStub.image.id })).resolves.toBeUndefined(); + await expect(sut.handleSidecarCheck({ id: 'non-existent' })).resolves.toBeUndefined(); expect(mocks.asset.update).not.toHaveBeenCalled(); }); diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 6627ffea8a..ee4b4ec05f 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,14 +1,16 @@ import { plainToInstance } from 'class-transformer'; import { defaults, SystemConfig } from 'src/config'; -import { AlbumUser } from 'src/database'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; import { NotificationService } from 'src/services/notification.service'; import { INotifyAlbumUpdateJob } from 'src/types'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { notificationStub } from 'test/fixtures/notification.stub'; import { userStub } from 'test/fixtures/user.stub'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; const configs = { @@ -267,14 +269,14 @@ describe(NotificationService.name, () => { }); it('should skip if recipient could not be found', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queue).not.toHaveBeenCalled(); }); it('should skip if the recipient has email notifications disabled', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -290,7 +292,7 @@ describe(NotificationService.name, () => { }); it('should skip if the recipient has email notifications for album invite disabled', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -306,7 +308,7 @@ describe(NotificationService.name, () => { }); it('should send invite email', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -328,7 +330,8 @@ describe(NotificationService.name, () => { }); it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const album = AlbumFactory.create({ albumThumbnailAssetId: newUuid() }); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -345,7 +348,7 @@ describe(NotificationService.name, () => { await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -358,7 +361,9 @@ describe(NotificationService.name, () => { }); it('should send invite email with album thumbnail as jpeg', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const assetFile = AssetFileFactory.create({ type: AssetFileType.Thumbnail }); + const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId }); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -371,13 +376,11 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ - { id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg', isEdited: false }, - ]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetFile]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -390,7 +393,9 @@ describe(NotificationService.name, () => { }); it('should send invite email with album thumbnail and arbitrary extension', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).asset(asset).build(); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -403,18 +408,18 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([{ ...assetStub.image.files[2], isEdited: false }]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([asset.files[0]]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SendMail, data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album'), - imageAttachments: [{ filename: 'album-thumbnail.ext', path: expect.anything(), cid: expect.anything() }], + imageAttachments: [{ filename: 'album-thumbnail.jpg', path: expect.anything(), cid: expect.anything() }], }), }); }); @@ -427,85 +432,74 @@ describe(NotificationService.name, () => { }); it('should skip if owner could not be found', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + mocks.album.getById.mockResolvedValue(AlbumFactory.create({ ownerId: 'non-existent' })); await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped); expect(mocks.systemMetadata.get).not.toHaveBeenCalled(); }); it('should skip recipient that could not be looked up', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValueOnce(userStub.user1); + const album = AlbumFactory.from().albumUser({ userId: 'non-existent' }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValueOnce(album.owner); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: 'non-existent' }); + expect(mocks.user.get).toHaveBeenCalledWith('non-existent', { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue({ - ...userStub.user1, - metadata: [ - { - key: UserMetadataKey.Preferences, - value: { emailNotifications: { enabled: false, albumUpdate: true } }, - }, - ], - }); + const user = UserFactory.from() + .metadata({ + key: UserMetadataKey.Preferences, + value: { emailNotifications: { enabled: false, albumUpdate: true } }, + }) + .build(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications for the album update event', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue({ - ...userStub.user1, - metadata: [ - { - key: UserMetadataKey.Preferences, - value: { emailNotifications: { enabled: true, albumUpdate: false } }, - }, - ], - }); + const user = UserFactory.from() + .metadata({ + key: UserMetadataKey.Preferences, + value: { emailNotifications: { enabled: true, albumUpdate: false } }, + }) + .build(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should send email', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue(userStub.user1); + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalled(); }); diff --git a/server/src/services/ocr.service.spec.ts b/server/src/services/ocr.service.spec.ts index 404f423cac..d5b146e942 100644 --- a/server/src/services/ocr.service.spec.ts +++ b/server/src/services/ocr.service.spec.ts @@ -1,6 +1,6 @@ -import { AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; +import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { OcrService } from 'src/services/ocr.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; @@ -14,7 +14,7 @@ describe(OcrService.name, () => { mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); mocks.assetJob.getForOcr.mockResolvedValue({ visibility: AssetVisibility.Timeline, - previewFile: assetStub.image.files[1].path, + previewFile: '/uploads/user-id/thumbs/path.jpg', }); }); @@ -41,20 +41,22 @@ describe(OcrService.name, () => { }); it('should queue the assets without ocr', async () => { - mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([asset])); await sut.handleQueueOcr({ force: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: assetStub.image.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: asset.id } }]); expect(mocks.assetJob.streamForOcrJob).toHaveBeenCalledWith(false); }); it('should queue all the assets', async () => { - mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([asset])); await sut.handleQueueOcr({ force: true }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: assetStub.image.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: asset.id } }]); expect(mocks.assetJob.streamForOcrJob).toHaveBeenCalledWith(true); }); }); @@ -70,15 +72,17 @@ describe(OcrService.name, () => { }); it('should skip assets without a resize path', async () => { + const asset = AssetFactory.create(); mocks.assetJob.getForOcr.mockResolvedValue({ visibility: AssetVisibility.Timeline, previewFile: null }); - expect(await sut.handleOcr({ id: assetStub.noResizePath.id })).toEqual(JobStatus.Failed); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Failed); expect(mocks.ocr.upsert).not.toHaveBeenCalled(); expect(mocks.machineLearning.ocr).not.toHaveBeenCalled(); }); it('should save the returned objects', async () => { + const asset = AssetFactory.create(); mocks.machineLearning.ocr.mockResolvedValue({ box: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160], boxScore: [0.9, 0.8], @@ -86,7 +90,7 @@ describe(OcrService.name, () => { textScore: [0.95, 0.85], }); - expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.ocr).toHaveBeenCalledWith( '/uploads/user-id/thumbs/path.jpg', @@ -98,10 +102,10 @@ describe(OcrService.name, () => { }), ); expect(mocks.ocr.upsert).toHaveBeenCalledWith( - assetStub.image.id, + asset.id, [ { - assetId: assetStub.image.id, + assetId: asset.id, boxScore: 0.9, text: 'One Two Three', textScore: 0.95, @@ -115,7 +119,7 @@ describe(OcrService.name, () => { y4: 80, }, { - assetId: assetStub.image.id, + assetId: asset.id, boxScore: 0.8, text: 'Four Five', textScore: 0.85, @@ -134,6 +138,7 @@ describe(OcrService.name, () => { }); it('should apply config settings', async () => { + const asset = AssetFactory.create(); mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { enabled: true, @@ -148,7 +153,7 @@ describe(OcrService.name, () => { }); mockOcrResult(); - expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.ocr).toHaveBeenCalledWith( '/uploads/user-id/thumbs/path.jpg', @@ -159,16 +164,17 @@ describe(OcrService.name, () => { maxResolution: 1500, }), ); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, [], ''); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, [], ''); }); it('should skip invisible assets', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.assetJob.getForOcr.mockResolvedValue({ visibility: AssetVisibility.Hidden, - previewFile: assetStub.image.files[1].path, + previewFile: asset.files[0].path, }); - expect(await sut.handleOcr({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Skipped); expect(mocks.machineLearning.ocr).not.toHaveBeenCalled(); expect(mocks.ocr.upsert).not.toHaveBeenCalled(); @@ -177,7 +183,7 @@ describe(OcrService.name, () => { it('should fail if asset could not be found', async () => { mocks.assetJob.getForOcr.mockResolvedValue(void 0); - expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Failed); + expect(await sut.handleOcr({ id: 'non-existent' })).toEqual(JobStatus.Failed); expect(mocks.machineLearning.ocr).not.toHaveBeenCalled(); expect(mocks.ocr.upsert).not.toHaveBeenCalled(); @@ -185,79 +191,84 @@ describe(OcrService.name, () => { describe('search tokenization', () => { it('should generate bigrams for Chinese text', async () => { + const asset = AssetFactory.create(); mockOcrResult('機器學習'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 器學 學習'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '機器 器學 學習'); }); it('should generate bigrams for Japanese text', async () => { + const asset = AssetFactory.create(); mockOcrResult('テスト'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'テス スト'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'テス スト'); }); it('should generate bigrams for Korean text', async () => { + const asset = AssetFactory.create(); mockOcrResult('한국어'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '한국 국어'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '한국 국어'); }); it('should pass through Latin text unchanged', async () => { + const asset = AssetFactory.create(); mockOcrResult('Hello World'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'Hello World'); }); it('should handle mixed CJK and Latin text', async () => { + const asset = AssetFactory.create(); mockOcrResult('機器學習Model'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 器學 學習 Model'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '機器 器學 學習 Model'); }); it('should handle year followed by CJK', async () => { + const asset = AssetFactory.create(); mockOcrResult('2024年レポート'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith( - assetStub.image.id, - expect.any(Array), - '2024 年レ レポ ポー ート', - ); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '2024 年レ レポ ポー ート'); }); it('should join multiple OCR boxes', async () => { + const asset = AssetFactory.create(); mockOcrResult('機器', 'Learning'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 Learning'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '機器 Learning'); }); it('should normalize whitespace', async () => { + const asset = AssetFactory.create(); mockOcrResult(' Hello World '); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'Hello World'); }); it('should keep single CJK characters', async () => { + const asset = AssetFactory.create(); mockOcrResult('A', '中', 'B'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'A 中 B'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'A 中 B'); }); }); }); diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index b57a5e1072..0928b57f97 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -1,12 +1,12 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { mapFaces, mapPerson, PersonResponseDto } from 'src/dtos/person.dto'; -import { CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum'; +import { AssetFileType, CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum'; import { DetectedFaces } from 'src/repositories/machine-learning.repository'; import { FaceSearchResult } from 'src/repositories/search.repository'; import { PersonService } from 'src/services/person.service'; import { ImmichFileResponse } from 'src/utils/file'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { personStub } from 'test/fixtures/person.stub'; @@ -261,7 +261,7 @@ describe(PersonService.name, () => { it("should update a person's thumbnailPath", async () => { mocks.person.update.mockResolvedValue(personStub.withName); mocks.person.getFacesByIds.mockResolvedValue([faceStub.face1]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId])); mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect( @@ -331,7 +331,7 @@ describe(PersonService.name, () => { await expect( sut.reassignFaces(authStub.admin, personStub.noName.id, { - data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }], + data: [{ personId: personStub.withName.id, assetId: faceStub.face1.assetId }], }), ).resolves.toBeDefined(); @@ -352,9 +352,10 @@ describe(PersonService.name, () => { describe('getFacesById', () => { it('should get the bounding boxes for an asset', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId])); + const asset = AssetFactory.from({ id: faceStub.face1.assetId }).exif().build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.person.getFaces.mockResolvedValue([faceStub.primaryFace1]); - mocks.asset.getById.mockResolvedValue(assetStub.image); + mocks.asset.getById.mockResolvedValue(asset); await expect(sut.getFacesById(authStub.admin, { id: faceStub.face1.assetId })).resolves.toStrictEqual([ mapFaces(faceStub.primaryFace1, authStub.admin), ]); @@ -455,7 +456,8 @@ describe(PersonService.name, () => { }); it('should queue missing assets', async () => { - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); await sut.handleQueueDetectFaces({ force: false }); @@ -464,13 +466,14 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should queue all assets', async () => { - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.withName]); await sut.handleQueueDetectFaces({ force: true }); @@ -483,13 +486,14 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should refresh all assets', async () => { - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); await sut.handleQueueDetectFaces({ force: undefined }); @@ -501,16 +505,17 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PersonCleanup }); }); it('should delete existing people and faces if forced', async () => { + const asset = AssetFactory.create(); mocks.person.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson])); mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); mocks.person.deleteFaces.mockResolvedValue(); @@ -520,7 +525,7 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); @@ -718,26 +723,28 @@ describe(PersonService.name, () => { }); it('should skip when no resize path', async () => { - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.noResizePath, files: [] }); - await sut.handleDetectFaces({ id: assetStub.noResizePath.id }); + const asset = AssetFactory.create(); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); + await sut.handleDetectFaces({ id: asset.id }); expect(mocks.machineLearning.detectFaces).not.toHaveBeenCalled(); }); it('should handle no results', async () => { const start = Date.now(); + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue({ imageHeight: 500, imageWidth: 400, faces: [] }); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); - await sut.handleDetectFaces({ id: assetStub.image.id }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); + await sut.handleDetectFaces({ id: asset.id }); expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith( - '/uploads/user-id/thumbs/path.jpg', + asset.files[0].path, expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), ); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({ - assetId: assetStub.image.id, + assetId: asset.id, facesRecognizedAt: expect.any(Date), }); const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date; @@ -745,14 +752,15 @@ describe(PersonService.name, () => { }); it('should create a face with no person and queue recognition job', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); mocks.search.searchFaces.mockResolvedValue([{ ...faceStub.face1, distance: 0.7 }]); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); mocks.person.refreshFaces.mockResolvedValue(); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith([{ ...face, assetId: asset.id }], [], [faceSearch]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, { name: JobName.FacialRecognition, data: { id: faceId } }, @@ -762,14 +770,11 @@ describe(PersonService.name, () => { }); it('should delete an existing face not among the new detected faces', async () => { + const asset = AssetFactory.from().face(faceStub.primaryFace1).file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue({ faces: [], imageHeight: 500, imageWidth: 400 }); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.primaryFace1], - files: [assetStub.image.files[1]], - }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); expect(mocks.person.refreshFaces).toHaveBeenCalledWith([], [faceStub.primaryFace1.id], []); expect(mocks.job.queueAll).not.toHaveBeenCalled(); @@ -778,17 +783,18 @@ describe(PersonService.name, () => { }); it('should add new face and delete an existing face not among the new detected faces', async () => { + const asset = AssetFactory.from().face(faceStub.primaryFace1).file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.primaryFace1], - files: [assetStub.image.files[1]], - }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); mocks.person.refreshFaces.mockResolvedValue(); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [faceStub.primaryFace1.id], [faceSearch]); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith( + [{ ...face, assetId: asset.id }], + [faceStub.primaryFace1.id], + [faceSearch], + ); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, { name: JobName.FacialRecognition, data: { id: faceId } }, @@ -798,15 +804,12 @@ describe(PersonService.name, () => { }); it('should add embedding to matching metadata face', async () => { + const asset = AssetFactory.from().face(faceStub.fromExif1).file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.fromExif1], - files: [assetStub.image.files[1]], - }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); mocks.person.refreshFaces.mockResolvedValue(); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [], @@ -819,16 +822,13 @@ describe(PersonService.name, () => { }); it('should not add embedding to non-matching metadata face', async () => { + const asset = AssetFactory.from().face(faceStub.fromExif2).file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.fromExif2], - files: [assetStub.image.files[1]], - }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith([{ ...face, assetId: asset.id }], [], [faceSearch]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, { name: JobName.FacialRecognition, data: { id: faceId } }, diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 52a4e6048f..e63dcedb7d 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -677,8 +677,9 @@ export class PersonService extends BaseService { }; // now coordinates are in original image space - dto.imageHeight = asset.exifInfo.exifImageHeight; - dto.imageWidth = asset.exifInfo.exifImageWidth; + const originalDimensions = getDimensions(asset.exifInfo); + dto.imageWidth = originalDimensions.width; + dto.imageHeight = originalDimensions.height; } await this.personRepository.createAssetFace({ diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 0dec02f18f..5f1125eaed 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -2,7 +2,8 @@ import { BadRequestException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { SearchSuggestionType } from 'src/dtos/search.dto'; import { SearchService } from 'src/services/search.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { personStub } from 'test/fixtures/person.stub'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -64,16 +65,18 @@ describe(SearchService.name, () => { describe('getExploreData', () => { it('should get assets by city and tag', async () => { + const auth = AuthFactory.create(); + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); mocks.asset.getAssetIdByCity.mockResolvedValue({ fieldName: 'exifInfo.city', - items: [{ value: 'test-city', data: assetStub.withLocation.id }], + items: [{ value: 'city', data: asset.id }], }); - mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.withLocation]); - const expectedResponse = [ - { fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] }, - ]; + mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([asset as never]); + const expectedResponse = [{ fieldName: 'exifInfo.city', items: [{ value: 'city', data: mapAsset(asset) }] }]; - const result = await sut.getExploreData(authStub.user1); + const result = await sut.getExploreData(auth); expect(result).toEqual(expectedResponse); }); diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 90c212650e..5ad145af2b 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -1,10 +1,10 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; -import _ from 'lodash'; import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { SharedLinkType } from 'src/enum'; import { SharedLinkService } from 'src/services/shared-link.service'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { SharedLinkFactory } from 'test/factories/shared-link.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { factory } from 'test/small.factory'; @@ -35,14 +35,14 @@ describe(SharedLinkService.name, () => { describe('getMine', () => { it('should only work for a public user', async () => { - await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException); + await expect(sut.getMine(authStub.admin, [])).rejects.toBeInstanceOf(ForbiddenException); expect(mocks.sharedLink.get).not.toHaveBeenCalled(); }); it('should return the shared link for the public user', async () => { const authDto = authStub.adminSharedLink; mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid); - await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid); + await expect(sut.getMine(authDto, [])).resolves.toEqual(sharedLinkResponseStub.valid); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); @@ -55,21 +55,22 @@ describe(SharedLinkService.name, () => { }, }); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); - const response = await sut.getMine(authDto, {}); + const response = await sut.getMine(authDto, []); expect(response.assets[0]).toMatchObject({ hasMetadata: false }); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); - it('should throw an error for an invalid password protected shared link', async () => { + it('should throw an error for a request without a shared link auth token', async () => { const authDto = authStub.adminSharedLink; mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.passwordRequired); - await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect(sut.getMine(authDto, [])).rejects.toBeInstanceOf(UnauthorizedException); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); - it('should allow a correct password on a password protected shared link', async () => { + it('should accept a valid shared link auth token', async () => { mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' }); - await expect(sut.getMine(authStub.adminSharedLink, { password: '123' })).resolves.toBeDefined(); + mocks.crypto.hashSha256.mockReturnValue('hashed-auth-token'); + await expect(sut.getMine(authStub.adminSharedLink, ['hashed-auth-token'])).resolves.toBeDefined(); expect(mocks.sharedLink.get).toHaveBeenCalledWith( authStub.adminSharedLink.user.id, authStub.adminSharedLink.sharedLink?.id, @@ -120,19 +121,17 @@ describe(SharedLinkService.name, () => { }); it('should create an album shared link', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); + const album = AlbumFactory.from().asset().build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.valid); - await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: albumStub.oneAsset.id }); + await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: album.id }); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([albumStub.oneAsset.id]), - ); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([album.id])); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ type: SharedLinkType.Album, userId: authStub.admin.user.id, - albumId: albumStub.oneAsset.id, + albumId: album.id, allowDownload: true, allowUpload: true, description: null, @@ -144,12 +143,13 @@ describe(SharedLinkService.name, () => { }); it('should create an individual shared link', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { type: SharedLinkType.Individual, - assetIds: [assetStub.image.id], + assetIds: [asset.id], showMetadata: true, allowDownload: true, allowUpload: true, @@ -157,7 +157,7 @@ describe(SharedLinkService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set([assetStub.image.id]), + new Set([asset.id]), false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ @@ -167,7 +167,7 @@ describe(SharedLinkService.name, () => { allowDownload: true, slug: null, allowUpload: true, - assetIds: [assetStub.image.id], + assetIds: [asset.id], description: null, expiresAt: null, showExif: true, @@ -176,12 +176,13 @@ describe(SharedLinkService.name, () => { }); it('should create a shared link with allowDownload set to false when showMetadata is false', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { type: SharedLinkType.Individual, - assetIds: [assetStub.image.id], + assetIds: [asset.id], showMetadata: false, allowDownload: true, allowUpload: true, @@ -189,7 +190,7 @@ describe(SharedLinkService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set([assetStub.image.id]), + new Set([asset.id]), false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ @@ -198,7 +199,7 @@ describe(SharedLinkService.name, () => { albumId: null, allowDownload: false, allowUpload: true, - assetIds: [assetStub.image.id], + assetIds: [asset.id], description: null, expiresAt: null, showExif: false, @@ -265,25 +266,28 @@ describe(SharedLinkService.name, () => { }); it('should add assets to a shared link', async () => { - mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); - mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); - mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3'])); + const asset = AssetFactory.create(); + const sharedLink = SharedLinkFactory.from().asset(asset).build(); + const newAsset = AssetFactory.create(); + mocks.sharedLink.get.mockResolvedValue(sharedLink); + mocks.sharedLink.create.mockResolvedValue(sharedLink); + mocks.sharedLink.update.mockResolvedValue(sharedLink); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([newAsset.id])); await expect( - sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2', 'asset-3'] }), + sut.addAssets(authStub.admin, sharedLink.id, { assetIds: [asset.id, 'asset-2', newAsset.id] }), ).resolves.toEqual([ - { assetId: assetStub.image.id, success: false, error: AssetIdErrorReason.DUPLICATE }, + { assetId: asset.id, success: false, error: AssetIdErrorReason.DUPLICATE }, { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NO_PERMISSION }, - { assetId: 'asset-3', success: true }, + { assetId: newAsset.id, success: true }, ]); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1); expect(mocks.sharedLink.update).toHaveBeenCalled(); expect(mocks.sharedLink.update).toHaveBeenCalledWith({ - ...sharedLinkStub.individual, + ...sharedLink, slug: null, - assetIds: ['asset-3'], + assetIds: [newAsset.id], }); }); }); @@ -298,20 +302,22 @@ describe(SharedLinkService.name, () => { }); it('should remove assets from a shared link', async () => { - mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); - mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); - mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual); - mocks.sharedLinkAsset.remove.mockResolvedValue([assetStub.image.id]); + const asset = AssetFactory.create(); + const sharedLink = SharedLinkFactory.from().asset(asset).build(); + mocks.sharedLink.get.mockResolvedValue(sharedLink); + mocks.sharedLink.create.mockResolvedValue(sharedLink); + mocks.sharedLink.update.mockResolvedValue(sharedLink); + mocks.sharedLinkAsset.remove.mockResolvedValue([asset.id]); await expect( - sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }), + sut.removeAssets(authStub.admin, sharedLink.id, { assetIds: [asset.id, 'asset-2'] }), ).resolves.toEqual([ - { assetId: assetStub.image.id, success: true }, + { assetId: asset.id, success: true }, { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, ]); - expect(mocks.sharedLinkAsset.remove).toHaveBeenCalledWith('link-1', [assetStub.image.id, 'asset-2']); - expect(mocks.sharedLink.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] }); + expect(mocks.sharedLinkAsset.remove).toHaveBeenCalledWith(sharedLink.id, [asset.id, 'asset-2']); + expect(mocks.sharedLink.update).toHaveBeenCalledWith(expect.objectContaining({ assets: [] })); }); }); @@ -335,7 +341,7 @@ describe(SharedLinkService.name, () => { await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ description: '1 shared photos & videos', - imageUrl: `https://my.immich.app/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, + imageUrl: `https://my.immich.app/api/assets/${sharedLinkStub.individual.assets[0].id}/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, title: 'Public Share', }); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 1440598084..e321e4990d 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; import { PostgresError } from 'postgres'; -import { SharedLink } from 'src/database'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -8,7 +7,7 @@ import { mapSharedLink, SharedLinkCreateDto, SharedLinkEditDto, - SharedLinkPasswordDto, + SharedLinkLoginDto, SharedLinkResponseDto, SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; @@ -24,18 +23,41 @@ export class SharedLinkService extends BaseService { .then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false }))); } - async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise { + async login(auth: AuthDto, dto: SharedLinkLoginDto) { if (!auth.sharedLink) { throw new ForbiddenException(); } const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id); - const response = mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }); - if (sharedLink.password) { - response.token = this.validateAndRefreshToken(sharedLink, dto); + const { id, password } = sharedLink; + + if (!password) { + throw new BadRequestException('Shared link is not password protected'); } - return response; + if (password !== dto.password) { + throw new UnauthorizedException('Invalid password'); + } + + return { + sharedLink: mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }), + token: this.asToken({ id, password }), + }; + } + + async getMine(auth: AuthDto, authTokens: string[]) { + if (!auth.sharedLink) { + throw new ForbiddenException(); + } + + const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id); + const { id, password } = sharedLink; + + if (password && !authTokens.includes(this.asToken({ id, password }))) { + throw new UnauthorizedException('Password required'); + } + + return mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }); } async get(auth: AuthDto, id: string): Promise { @@ -213,16 +235,7 @@ export class SharedLinkService extends BaseService { }; } - private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string { - const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`); - const sharedLinkTokens = dto.token?.split(',') || []; - if (sharedLink.password !== dto.password && !sharedLinkTokens.includes(token)) { - throw new UnauthorizedException('Invalid password'); - } - - if (!sharedLinkTokens.includes(token)) { - sharedLinkTokens.push(token); - } - return sharedLinkTokens.join(','); + private asToken(sharedLink: { id: string; password: string }) { + return this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`); } } diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index b3af5cd15f..6bd0a3c9b2 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,8 +1,8 @@ import { SystemConfig } from 'src/config'; -import { ImmichWorker, JobName, JobStatus } from 'src/enum'; +import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { SmartInfoService } from 'src/services/smart-info.service'; import { getCLIPModelInfo } from 'src/utils/misc'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; @@ -13,7 +13,7 @@ describe(SmartInfoService.name, () => { beforeEach(() => { ({ sut, mocks } = newTestService(SmartInfoService)); - mocks.asset.getByIds.mockResolvedValue([assetStub.image]); + mocks.asset.getByIds.mockResolvedValue([AssetFactory.create()]); mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); }); @@ -155,25 +155,23 @@ describe(SmartInfoService.name, () => { }); it('should queue the assets without clip embeddings', async () => { - mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([asset])); await sut.handleQueueEncodeClip({ force: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SmartSearch, data: { id: assetStub.image.id } }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SmartSearch, data: { id: asset.id } }]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(false); expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); }); it('should queue all the assets', async () => { - mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([asset])); await sut.handleQueueEncodeClip({ force: true }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SmartSearch, data: { id: assetStub.image.id } }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SmartSearch, data: { id: asset.id } }]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(true); expect(mocks.database.setDimensionSize).toHaveBeenCalledExactlyOnceWith(512); }); @@ -190,34 +188,36 @@ describe(SmartInfoService.name, () => { }); it('should skip assets without a resize path', async () => { - mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.noResizePath, files: [] }); + const asset = AssetFactory.create(); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.Failed); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Failed); expect(mocks.search.upsert).not.toHaveBeenCalled(); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); }); it('should save the returned objects', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]'); - mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( - '/uploads/user-id/thumbs/path.jpg', + asset.files[0].path, expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); - expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]'); + expect(mocks.search.upsert).toHaveBeenCalledWith(asset.id, '[0.01, 0.02, 0.03]'); }); it('should skip invisible assets', async () => { - mocks.assetJob.getForClipEncoding.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - files: [assetStub.image.files[1]], - }); + const asset = AssetFactory.from({ visibility: AssetVisibility.Hidden }) + .file({ type: AssetFileType.Preview }) + .build(); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Skipped); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); expect(mocks.search.upsert).not.toHaveBeenCalled(); @@ -226,25 +226,26 @@ describe(SmartInfoService.name, () => { it('should fail if asset could not be found', async () => { mocks.assetJob.getForClipEncoding.mockResolvedValue(void 0); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Failed); + expect(await sut.handleEncodeClip({ id: 'non-existent' })).toEqual(JobStatus.Failed); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); expect(mocks.search.upsert).not.toHaveBeenCalled(); }); it('should wait for database', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]'); mocks.database.isBusy.mockReturnValue(true); - mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.database.wait).toHaveBeenCalledWith(512); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( - '/uploads/user-id/thumbs/path.jpg', + asset.files[0].path, expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); - expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]'); + expect(mocks.search.upsert).toHaveBeenCalledWith(asset.id, '[0.01, 0.02, 0.03]'); }); }); diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index 1dc87f4348..93f84e28e1 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -1,6 +1,8 @@ import { BadRequestException } from '@nestjs/common'; import { StackService } from 'src/services/stack.service'; -import { assetStub, stackStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; +import { StackFactory } from 'test/factories/stack.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -19,43 +21,49 @@ describe(StackService.name, () => { describe('search', () => { it('should search stacks', async () => { - mocks.stack.search.mockResolvedValue([stackStub('stack-id', [assetStub.image])]); + const auth = AuthFactory.create(); + const asset = AssetFactory.create(); + const stack = StackFactory.from().primaryAsset(asset).build(); + mocks.stack.search.mockResolvedValue([stack]); - await sut.search(authStub.admin, { primaryAssetId: assetStub.image.id }); + await sut.search(auth, { primaryAssetId: asset.id }); expect(mocks.stack.search).toHaveBeenCalledWith({ - ownerId: authStub.admin.user.id, - primaryAssetId: assetStub.image.id, + ownerId: auth.user.id, + primaryAssetId: asset.id, }); }); }); describe('create', () => { it('should require asset.update permissions', async () => { - await expect( - sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }), - ).rejects.toBeInstanceOf(BadRequestException); + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + + await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.stack.create).not.toHaveBeenCalled(); }); it('should create a stack', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id, assetStub.image1.id])); - mocks.stack.create.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); - await expect( - sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }), - ).resolves.toEqual({ - id: 'stack-id', - primaryAssetId: assetStub.image.id, - assets: [ - expect.objectContaining({ id: assetStub.image.id }), - expect.objectContaining({ id: assetStub.image1.id }), - ], + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([primaryAsset.id, asset.id])); + mocks.stack.create.mockResolvedValue(stack); + + await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({ + id: stack.id, + primaryAssetId: primaryAsset.id, + assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })], }); expect(mocks.event.emit).toHaveBeenCalledWith('StackCreate', { - stackId: 'stack-id', - userId: authStub.admin.user.id, + stackId: stack.id, + userId: auth.user.id, }); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled(); }); @@ -79,25 +87,26 @@ describe(StackService.name, () => { }); it('should get stack', async () => { - mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build(); - await expect(sut.get(authStub.admin, 'stack-id')).resolves.toEqual({ - id: 'stack-id', - primaryAssetId: assetStub.image.id, - assets: [ - expect.objectContaining({ id: assetStub.image.id }), - expect.objectContaining({ id: assetStub.image1.id }), - ], + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); + mocks.stack.getById.mockResolvedValue(stack); + + await expect(sut.get(auth, stack.id)).resolves.toEqual({ + id: stack.id, + primaryAssetId: primaryAsset.id, + assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })], }); expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled(); - expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); + expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); }); }); describe('update', () => { it('should require stack.update permissions', async () => { - await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException); expect(mocks.stack.getById).not.toHaveBeenCalled(); expect(mocks.stack.update).not.toHaveBeenCalled(); @@ -107,7 +116,7 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(Error); + await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(Error); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); expect(mocks.stack.update).not.toHaveBeenCalled(); @@ -115,55 +124,64 @@ describe(StackService.name, () => { }); it('should fail if the provided primary asset id is not in the stack', async () => { - mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); + const auth = AuthFactory.create(); + const stack = StackFactory.from().primaryAsset().asset().build(); - await expect(sut.update(authStub.admin, 'stack-id', { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf( + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); + mocks.stack.getById.mockResolvedValue(stack); + + await expect(sut.update(auth, stack.id, { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf( BadRequestException, ); - expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); + expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); expect(mocks.stack.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); }); it('should update stack', async () => { - mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); - mocks.stack.update.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build(); - await sut.update(authStub.admin, 'stack-id', { primaryAssetId: assetStub.image1.id }); + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); + mocks.stack.getById.mockResolvedValue(stack); + mocks.stack.update.mockResolvedValue(stack); - expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); - expect(mocks.stack.update).toHaveBeenCalledWith('stack-id', { - id: 'stack-id', - primaryAssetId: assetStub.image1.id, + await sut.update(auth, stack.id, { primaryAssetId: asset.id }); + + expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); + expect(mocks.stack.update).toHaveBeenCalledWith(stack.id, { + id: stack.id, + primaryAssetId: asset.id, }); expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', { - stackId: 'stack-id', - userId: authStub.admin.user.id, + stackId: stack.id, + userId: auth.user.id, }); }); }); describe('delete', () => { it('should require stack.delete permissions', async () => { - await expect(sut.delete(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.delete(AuthFactory.create(), 'stack-id')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.stack.delete).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); }); it('should delete stack', async () => { + const auth = AuthFactory.create(); + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); mocks.stack.delete.mockResolvedValue(); - await sut.delete(authStub.admin, 'stack-id'); + await sut.delete(auth, 'stack-id'); expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id'); expect(mocks.event.emit).toHaveBeenCalledWith('StackDelete', { stackId: 'stack-id', - userId: authStub.admin.user.id, + userId: auth.user.id, }); }); }); @@ -214,24 +232,26 @@ describe(StackService.name, () => { }); it('should fail if the assetId is the primaryAssetId', async () => { + const asset = AssetFactory.create(); mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: assetStub.image.id }); + mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: asset.id }); - await expect( - sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.image.id }), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: asset.id })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); }); it("should update the asset to nullify it's stack-id", async () => { + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: assetStub.image.id }); + mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: primaryAsset.id }); - await sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.image1.id }); + await sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: asset.id }); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.image1.id, stackId: null }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, stackId: null }); expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', { stackId: 'stack-id', userId: authStub.admin.user.id, diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 0b5d538cea..09e0c10b80 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,16 +1,14 @@ import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; -import { AssetPathType, JobStatus } from 'src/enum'; +import { AssetPathType, AssetType, JobStatus } from 'src/enum'; import { StorageTemplateService } from 'src/services/storage-template.service'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { userStub } from 'test/fixtures/user.stub'; -import { factory } from 'test/small.factory'; +import { getForStorageTemplate } from 'test/mappers'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; -const motionAsset = assetStub.storageAsset({}); -const stillAsset = assetStub.storageAsset({ livePhotoVideoId: motionAsset.id }); - describe(StorageTemplateService.name, () => { let sut: StorageTemplateService; let mocks: ServiceMocks; @@ -110,12 +108,27 @@ describe(StorageTemplateService.name, () => { }); it('should migrate single moving picture', async () => { + const motionAsset = AssetFactory.from({ + type: AssetType.Video, + + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const stillAsset = AssetFactory.from({ + livePhotoVideoId: motionAsset.id, + + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + mocks.user.get.mockResolvedValue(userStub.user1); - const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; + const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName.slice(0, -4)}.mp4`; const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(stillAsset); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset)); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset)); mocks.move.create.mockResolvedValueOnce({ id: '123', @@ -141,16 +154,16 @@ describe(StorageTemplateService.name, () => { }); it('should use handlebar if condition for album', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; - const album = albumStub.oneAsset; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); + const album = AlbumFactory.from().asset().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); mocks.album.getByAssetId.mockResolvedValueOnce([album]); expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); @@ -166,14 +179,14 @@ describe(StorageTemplateService.name, () => { }); it('should use handlebar else condition for album', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}'; sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); @@ -189,9 +202,9 @@ describe(StorageTemplateService.name, () => { }); it('should handle album startDate', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; - const album = albumStub.oneAsset; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); + const album = AlbumFactory.from().asset().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}'; @@ -199,7 +212,7 @@ describe(StorageTemplateService.name, () => { sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); mocks.album.getByAssetId.mockResolvedValueOnce([album]); mocks.album.getMetadataForIds.mockResolvedValueOnce([ { @@ -225,8 +238,8 @@ describe(StorageTemplateService.name, () => { }); it('should handle else condition from album startDate', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}'; @@ -234,7 +247,7 @@ describe(StorageTemplateService.name, () => { sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); @@ -248,11 +261,18 @@ describe(StorageTemplateService.name, () => { }); it('should migrate previously failed move from original path when it still exists', async () => { - mocks.user.get.mockResolvedValue(userStub.user1); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); - const asset = assetStub.storageAsset(); - const previousFailedNewPath = `/data/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`; - const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`; + mocks.user.get.mockResolvedValue(user); + + const previousFailedNewPath = `/data/library/${user.id}/2023/Feb/${asset.originalFileName}`; + const newPath = `/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === asset.originalPath)); mocks.move.getByEntity.mockResolvedValue({ @@ -262,7 +282,7 @@ describe(StorageTemplateService.name, () => { oldPath: asset.originalPath, newPath: previousFailedNewPath, }); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset)); mocks.move.update.mockResolvedValue({ id: '123', entityId: asset.id, @@ -288,9 +308,16 @@ describe(StorageTemplateService.name, () => { }); it('should migrate previously failed move from previous new path when old path no longer exists, should validate file size still matches before moving', async () => { - mocks.user.get.mockResolvedValue(userStub.user1); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif({ fileSizeInByte: 5000 }) + .build(); + + mocks.user.get.mockResolvedValue(user); - const asset = assetStub.storageAsset({ fileSizeInByte: 5000 }); const previousFailedNewPath = `/data/library/${asset.ownerId}/2022/June/${asset.originalFileName}`; const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; @@ -304,7 +331,7 @@ describe(StorageTemplateService.name, () => { oldPath: asset.originalPath, newPath: previousFailedNewPath, }); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset)); mocks.move.update.mockResolvedValue({ id: '123', entityId: asset.id, @@ -325,45 +352,53 @@ describe(StorageTemplateService.name, () => { }); it('should fail move if copying and hash of asset and the new file do not match', async () => { - mocks.user.get.mockResolvedValue(userStub.user1); - const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`; + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.user.get.mockResolvedValue(user); + const newPath = `/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats); mocks.crypto.hashFile.mockResolvedValue(Buffer.from('different-hash', 'utf8')); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(testAsset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset)); mocks.move.create.mockResolvedValue({ id: '123', - entityId: testAsset.id, + entityId: asset.id, pathType: AssetPathType.Original, - oldPath: testAsset.originalPath, + oldPath: asset.originalPath, newPath, }); - await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.Success); + await expect(sut.handleMigrationSingle({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(testAsset.id); + expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(asset.id); expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(1); expect(mocks.storage.stat).toHaveBeenCalledWith(newPath); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: testAsset.id, + entityId: asset.id, pathType: AssetPathType.Original, - oldPath: testAsset.originalPath, + oldPath: asset.originalPath, newPath, }); - expect(mocks.storage.rename).toHaveBeenCalledWith(testAsset.originalPath, newPath); - expect(mocks.storage.copyFile).toHaveBeenCalledWith(testAsset.originalPath, newPath); + expect(mocks.storage.rename).toHaveBeenCalledWith(asset.originalPath, newPath); + expect(mocks.storage.copyFile).toHaveBeenCalledWith(asset.originalPath, newPath); expect(mocks.storage.unlink).toHaveBeenCalledWith(newPath); expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); expect(mocks.asset.update).not.toHaveBeenCalled(); }); - const testAsset = assetStub.storageAsset(); + const testAsset = AssetFactory.from().exif({ fileSizeInByte: 12_345 }).build(); it.each` - failedPathChecksum | failedPathSize | reason - ${testAsset.checksum} | ${500} | ${'file size'} - ${Buffer.from('bad checksum', 'utf8')} | ${testAsset.fileSizeInByte} | ${'checksum'} + failedPathChecksum | failedPathSize | reason + ${testAsset.checksum} | ${500} | ${'file size'} + ${Buffer.from('bad checksum', 'utf8')} | ${testAsset.exifInfo.fileSizeInByte} | ${'checksum'} `( 'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails', async ({ failedPathChecksum, failedPathSize }) => { @@ -381,7 +416,7 @@ describe(StorageTemplateService.name, () => { oldPath: testAsset.originalPath, newPath: previousFailedNewPath, }); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(testAsset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(testAsset)); mocks.move.update.mockResolvedValue({ id: '123', entityId: testAsset.id, @@ -414,12 +449,17 @@ describe(StorageTemplateService.name, () => { }); it('should handle an asset with a duplicate destination', async () => { - const asset = assetStub.storageAsset(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const oldPath = asset.originalPath; - const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; const newPath2 = newPath.replace('.jpg', '+1.jpg'); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ id: '123', @@ -441,9 +481,13 @@ describe(StorageTemplateService.name, () => { }); it('should skip when an asset already matches the template', async () => { - const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg' }); + const asset = AssetFactory.from({ + originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg', + }) + .exif() + .build(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); await sut.handleMigration(); @@ -456,9 +500,13 @@ describe(StorageTemplateService.name, () => { }); it('should skip when an asset is probably a duplicate', async () => { - const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg' }); + const asset = AssetFactory.from({ + originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg', + }) + .exif() + .build(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); await sut.handleMigration(); @@ -471,16 +519,21 @@ describe(StorageTemplateService.name, () => { }); it('should move an asset', async () => { - const asset = assetStub.storageAsset(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const oldPath = asset.originalPath; - const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ id: '123', - entityId: assetStub.image.id, + entityId: asset.id, pathType: AssetPathType.Original, - oldPath: assetStub.image.originalPath, + oldPath: asset.originalPath, newPath, }); @@ -492,9 +545,15 @@ describe(StorageTemplateService.name, () => { }); it('should use the user storage label', async () => { - const user = factory.userAdmin({ storageLabel: 'label-1' }); - const asset = assetStub.storageAsset({ ownerId: user.id }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const user = UserFactory.create({ storageLabel: 'label-1' }); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -508,7 +567,7 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - '/original/path.jpg', + asset.originalPath, expect.stringContaining(`/data/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).toHaveBeenCalledWith({ @@ -520,10 +579,16 @@ describe(StorageTemplateService.name, () => { }); it('should copy the file if rename fails due to EXDEV (rename across filesystems)', async () => { - const asset = assetStub.storageAsset({ originalPath: '/path/to/original.jpg', fileSizeInByte: 5000 }); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + originalPath: '/path/to/original.jpg', + }) + .exif({ fileSizeInByte: 5000 }) + .build(); + const oldPath = asset.originalPath; - const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ @@ -561,10 +626,17 @@ describe(StorageTemplateService.name, () => { }); it('should not update the database if the move fails due to incorrect newPath filesize', async () => { - const asset = assetStub.storageAsset(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); - mocks.user.getList.mockResolvedValue([userStub.user1]); + mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, @@ -580,22 +652,29 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - '/original/path.jpg', - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + asset.originalPath, + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.copyFile).toHaveBeenCalledWith( - '/original/path.jpg', - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + asset.originalPath, + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.stat).toHaveBeenCalledWith( - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); it('should not update the database if the move fails', async () => { - const asset = assetStub.storageAsset(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.storage.rename.mockRejectedValue(new Error('Read only system')); mocks.storage.copyFile.mockRejectedValue(new Error('Read only system')); mocks.move.create.mockResolvedValue({ @@ -605,25 +684,37 @@ describe(StorageTemplateService.name, () => { oldPath: asset.originalPath, newPath: '', }); - mocks.user.getList.mockResolvedValue([userStub.user1]); + mocks.user.getList.mockResolvedValue([user]); await sut.handleMigration(); expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - '/original/path.jpg', - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + asset.originalPath, + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); it('should migrate live photo motion video alongside the still image', async () => { - const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; + const motionAsset = AssetFactory.from({ + type: AssetType.Video, + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const stillAsset = AssetFactory.from({ + livePhotoVideoId: motionAsset.id, + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName.slice(0, -4)}.mp4`; const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([stillAsset])); + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset)); mocks.move.create.mockResolvedValueOnce({ id: '123', @@ -653,13 +744,17 @@ describe(StorageTemplateService.name, () => { describe('file rename correctness', () => { it('should not create double extensions when filename has lower extension', async () => { - const user = factory.userAdmin({ storageLabel: 'label-1' }); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create({ storageLabel: 'label-1' }); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, originalFileName: 'IMG_7065.HEIC', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -679,13 +774,17 @@ describe(StorageTemplateService.name, () => { }); it('should not create double extensions when filename has uppercase extension', async () => { - const user = factory.userAdmin(); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, originalFileName: 'IMG_7065.HEIC', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif({ fileSizeInByte: 12_345 }) + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -705,13 +804,17 @@ describe(StorageTemplateService.name, () => { }); it('should normalize the filename to lowercase (JPEG > jpg)', async () => { - const user = factory.userAdmin(); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, originalFileName: 'IMG_7065.JPEG', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -731,13 +834,17 @@ describe(StorageTemplateService.name, () => { }); it('should normalize the filename to lowercase (JPG > jpg)', async () => { - const user = factory.userAdmin(); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: '/data/library/user-id/2022/2022-06-19/IMG_7065.JPG', originalFileName: 'IMG_7065.JPG', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 71cf0d0ce8..b443d31c7f 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { join } from 'node:path'; +import { ErrorMessages } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; import { @@ -114,9 +115,7 @@ export class StorageService extends BaseService { this.logger.log(`Media location changed (from=${previous}, to=${current})`); if (!path.startsWith(previous)) { - throw new Error( - 'Detected an inconsistent media location. For more information, see https://docs.immich.app/errors#inconsistent-media-location', - ); + throw new Error(ErrorMessages.InconsistentMediaLocation); } this.logger.warn( diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts index 5b50340a9f..395ff86099 100644 --- a/server/src/services/sync.service.spec.ts +++ b/server/src/services/sync.service.spec.ts @@ -1,6 +1,6 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { SyncService } from 'src/services/sync.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -22,10 +22,14 @@ describe(SyncService.name, () => { describe('getAllAssetsForUserFullSync', () => { it('should return a list of all assets owned by the user', async () => { - mocks.asset.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]); + const [asset1, asset2] = [ + AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(), + AssetFactory.from().owner(authStub.user1.user).build(), + ]; + mocks.asset.getAllForUserFullSync.mockResolvedValue([asset1, asset2]); await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([ - mapAsset(assetStub.external, mapAssetOpts), - mapAsset(assetStub.hasEncodedVideo, mapAssetOpts), + mapAsset(asset1, mapAssetOpts), + mapAsset(asset2, mapAssetOpts), ]); expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({ ownerId: authStub.user1.user.id, @@ -60,10 +64,9 @@ describe(SyncService.name, () => { }); it('should return a response requiring a full sync when there are too many changes', async () => { + const asset = AssetFactory.create(); mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue( - Array.from({ length: 10_000 }).fill(assetStub.image), - ); + mocks.asset.getChangedDeltaSync.mockResolvedValue(Array.from({ length: 10_000 }).fill(asset)); await expect( sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); @@ -72,15 +75,17 @@ describe(SyncService.name, () => { }); it('should return a response with changes and deletions', async () => { + const asset = AssetFactory.create({ ownerId: authStub.user1.user.id }); + const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue([assetStub.image1]); - mocks.audit.getAfter.mockResolvedValue([assetStub.external.id]); + mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]); + mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]); await expect( sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: false, - upserted: [mapAsset(assetStub.image1, mapAssetOpts)], - deleted: [assetStub.external.id], + upserted: [mapAsset(asset, mapAssetOpts)], + deleted: [deletedAsset.id], }); expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1); expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1); diff --git a/server/src/services/view.service.spec.ts b/server/src/services/view.service.spec.ts index 86bfcef734..7b26fb5eb3 100644 --- a/server/src/services/view.service.spec.ts +++ b/server/src/services/view.service.spec.ts @@ -1,6 +1,6 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { ViewService } from 'src/services/view.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -32,8 +32,8 @@ describe(ViewService.name, () => { it('should return assets by original path', async () => { const path = '/asset'; - const asset1 = { ...assetStub.image, originalPath: '/asset/path1' }; - const asset2 = { ...assetStub.image, originalPath: '/asset/path2' }; + const asset1 = AssetFactory.create({ originalPath: '/asset/path1' }); + const asset2 = AssetFactory.create({ originalPath: '/asset/path2' }); const mockAssets = [asset1, asset2]; diff --git a/server/src/sql-tools/comparers/column.comparer.spec.ts b/server/src/sql-tools/comparers/column.comparer.spec.ts index 0fd4ed74b5..ef2afb348a 100644 --- a/server/src/sql-tools/comparers/column.comparer.spec.ts +++ b/server/src/sql-tools/comparers/column.comparer.spec.ts @@ -15,7 +15,7 @@ const testColumn: DatabaseColumn = { describe('compareColumns', () => { describe('onExtra', () => { it('should work', () => { - expect(compareColumns.onExtra(testColumn)).toEqual([ + expect(compareColumns().onExtra(testColumn)).toEqual([ { tableName: 'table1', columnName: 'test', @@ -28,7 +28,7 @@ describe('compareColumns', () => { describe('onMissing', () => { it('should work', () => { - expect(compareColumns.onMissing(testColumn)).toEqual([ + expect(compareColumns().onMissing(testColumn)).toEqual([ { type: 'ColumnAdd', column: testColumn, @@ -40,14 +40,14 @@ describe('compareColumns', () => { describe('onCompare', () => { it('should work', () => { - expect(compareColumns.onCompare(testColumn, testColumn)).toEqual([]); + expect(compareColumns().onCompare(testColumn, testColumn)).toEqual([]); }); it('should detect a change in type', () => { const source: DatabaseColumn = { ...testColumn }; const target: DatabaseColumn = { ...testColumn, type: 'text' }; const reason = 'column type is different (character varying vs text)'; - expect(compareColumns.onCompare(source, target)).toEqual([ + expect(compareColumns().onCompare(source, target)).toEqual([ { columnName: 'test', tableName: 'table1', @@ -66,7 +66,7 @@ describe('compareColumns', () => { const source: DatabaseColumn = { ...testColumn, nullable: true }; const target: DatabaseColumn = { ...testColumn, nullable: true, default: "''" }; const reason = `default is different (null vs '')`; - expect(compareColumns.onCompare(source, target)).toEqual([ + expect(compareColumns().onCompare(source, target)).toEqual([ { columnName: 'test', tableName: 'table1', @@ -83,7 +83,7 @@ describe('compareColumns', () => { const source: DatabaseColumn = { ...testColumn, comment: 'new comment' }; const target: DatabaseColumn = { ...testColumn, comment: 'old comment' }; const reason = 'comment is different (new comment vs old comment)'; - expect(compareColumns.onCompare(source, target)).toEqual([ + expect(compareColumns().onCompare(source, target)).toEqual([ { columnName: 'test', tableName: 'table1', diff --git a/server/src/sql-tools/comparers/column.comparer.ts b/server/src/sql-tools/comparers/column.comparer.ts index d3033430ef..54ffb34ffa 100644 --- a/server/src/sql-tools/comparers/column.comparer.ts +++ b/server/src/sql-tools/comparers/column.comparer.ts @@ -1,98 +1,99 @@ import { asRenameKey, getColumnType, isDefaultEqual } from 'src/sql-tools/helpers'; import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types'; -export const compareColumns = { - getRenameKey: (column) => { - return asRenameKey([ - column.tableName, - column.type, - column.nullable, - column.default, - column.storage, - column.primary, - column.isArray, - column.length, - column.identity, - column.enumName, - column.numericPrecision, - column.numericScale, - ]); - }, - onRename: (source, target) => [ - { - type: 'ColumnRename', - tableName: source.tableName, - oldName: target.name, - newName: source.name, - reason: Reason.Rename, +export const compareColumns = () => + ({ + getRenameKey: (column) => { + return asRenameKey([ + column.tableName, + column.type, + column.nullable, + column.default, + column.storage, + column.primary, + column.isArray, + column.length, + column.identity, + column.enumName, + column.numericPrecision, + column.numericScale, + ]); }, - ], - onMissing: (source) => [ - { - type: 'ColumnAdd', - column: source, - reason: Reason.MissingInTarget, + onRename: (source, target) => [ + { + type: 'ColumnRename', + tableName: source.tableName, + oldName: target.name, + newName: source.name, + reason: Reason.Rename, + }, + ], + onMissing: (source) => [ + { + type: 'ColumnAdd', + column: source, + reason: Reason.MissingInTarget, + }, + ], + onExtra: (target) => [ + { + type: 'ColumnDrop', + tableName: target.tableName, + columnName: target.name, + reason: Reason.MissingInSource, + }, + ], + onCompare: (source, target) => { + const sourceType = getColumnType(source); + const targetType = getColumnType(target); + + const isTypeChanged = sourceType !== targetType; + + if (isTypeChanged) { + // TODO: convert between types via UPDATE when possible + return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`); + } + + const items: SchemaDiff[] = []; + if (source.nullable !== target.nullable) { + items.push({ + type: 'ColumnAlter', + tableName: source.tableName, + columnName: source.name, + changes: { + nullable: source.nullable, + }, + reason: `nullable is different (${source.nullable} vs ${target.nullable})`, + }); + } + + if (!isDefaultEqual(source, target)) { + items.push({ + type: 'ColumnAlter', + tableName: source.tableName, + columnName: source.name, + changes: { + default: String(source.default ?? 'NULL'), + }, + reason: `default is different (${source.default ?? 'null'} vs ${target.default})`, + }); + } + + if (source.comment !== target.comment) { + items.push({ + type: 'ColumnAlter', + tableName: source.tableName, + columnName: source.name, + changes: { + comment: String(source.comment), + }, + reason: `comment is different (${source.comment} vs ${target.comment})`, + }); + } + + return items; }, - ], - onExtra: (target) => [ - { - type: 'ColumnDrop', - tableName: target.tableName, - columnName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - const sourceType = getColumnType(source); - const targetType = getColumnType(target); - - const isTypeChanged = sourceType !== targetType; - - if (isTypeChanged) { - // TODO: convert between types via UPDATE when possible - return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`); - } - - const items: SchemaDiff[] = []; - if (source.nullable !== target.nullable) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - nullable: source.nullable, - }, - reason: `nullable is different (${source.nullable} vs ${target.nullable})`, - }); - } - - if (!isDefaultEqual(source, target)) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - default: String(source.default ?? 'NULL'), - }, - reason: `default is different (${source.default ?? 'null'} vs ${target.default})`, - }); - } - - if (source.comment !== target.comment) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - comment: String(source.comment), - }, - reason: `comment is different (${source.comment} vs ${target.comment})`, - }); - } - - return items; - }, -} satisfies Comparer; + }) satisfies Comparer; const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { return [ diff --git a/server/src/sql-tools/comparers/constraint.comparer.spec.ts b/server/src/sql-tools/comparers/constraint.comparer.spec.ts index b5da19e8df..216728f8c4 100644 --- a/server/src/sql-tools/comparers/constraint.comparer.spec.ts +++ b/server/src/sql-tools/comparers/constraint.comparer.spec.ts @@ -13,7 +13,7 @@ const testConstraint: DatabaseConstraint = { describe('compareConstraints', () => { describe('onExtra', () => { it('should work', () => { - expect(compareConstraints.onExtra(testConstraint)).toEqual([ + expect(compareConstraints().onExtra(testConstraint)).toEqual([ { type: 'ConstraintDrop', constraintName: 'test', @@ -26,7 +26,7 @@ describe('compareConstraints', () => { describe('onMissing', () => { it('should work', () => { - expect(compareConstraints.onMissing(testConstraint)).toEqual([ + expect(compareConstraints().onMissing(testConstraint)).toEqual([ { type: 'ConstraintAdd', constraint: testConstraint, @@ -38,14 +38,14 @@ describe('compareConstraints', () => { describe('onCompare', () => { it('should work', () => { - expect(compareConstraints.onCompare(testConstraint, testConstraint)).toEqual([]); + expect(compareConstraints().onCompare(testConstraint, testConstraint)).toEqual([]); }); it('should detect a change in type', () => { const source: DatabaseConstraint = { ...testConstraint }; const target: DatabaseConstraint = { ...testConstraint, columnNames: ['column1', 'column2'] }; const reason = 'Primary key columns are different: (column1 vs column1,column2)'; - expect(compareConstraints.onCompare(source, target)).toEqual([ + expect(compareConstraints().onCompare(source, target)).toEqual([ { constraintName: 'test', tableName: 'table1', diff --git a/server/src/sql-tools/comparers/constraint.comparer.ts b/server/src/sql-tools/comparers/constraint.comparer.ts index dda184039f..03128878d5 100644 --- a/server/src/sql-tools/comparers/constraint.comparer.ts +++ b/server/src/sql-tools/comparers/constraint.comparer.ts @@ -12,7 +12,7 @@ import { SchemaDiff, } from 'src/sql-tools/types'; -export const compareConstraints: Comparer = { +export const compareConstraints = (): Comparer => ({ getRenameKey: (constraint) => { switch (constraint.type) { case ConstraintType.PRIMARY_KEY: @@ -83,7 +83,7 @@ export const compareConstraints: Comparer = { } } }, -}; +}); const comparePrimaryKeyConstraint: CompareFunction = (source, target) => { if (!haveEqualColumns(source.columnNames, target.columnNames)) { diff --git a/server/src/sql-tools/comparers/enum.comparer.spec.ts b/server/src/sql-tools/comparers/enum.comparer.spec.ts index 82fc205662..d788c7cd71 100644 --- a/server/src/sql-tools/comparers/enum.comparer.spec.ts +++ b/server/src/sql-tools/comparers/enum.comparer.spec.ts @@ -7,7 +7,7 @@ const testEnum: DatabaseEnum = { name: 'test', values: ['foo', 'bar'], synchroni describe('compareEnums', () => { describe('onExtra', () => { it('should work', () => { - expect(compareEnums.onExtra(testEnum)).toEqual([ + expect(compareEnums().onExtra(testEnum)).toEqual([ { enumName: 'test', type: 'EnumDrop', @@ -19,7 +19,7 @@ describe('compareEnums', () => { describe('onMissing', () => { it('should work', () => { - expect(compareEnums.onMissing(testEnum)).toEqual([ + expect(compareEnums().onMissing(testEnum)).toEqual([ { type: 'EnumCreate', enum: testEnum, @@ -31,13 +31,13 @@ describe('compareEnums', () => { describe('onCompare', () => { it('should work', () => { - expect(compareEnums.onCompare(testEnum, testEnum)).toEqual([]); + expect(compareEnums().onCompare(testEnum, testEnum)).toEqual([]); }); it('should drop and recreate when values list is different', () => { const source = { name: 'test', values: ['foo', 'bar'], synchronize: true }; const target = { name: 'test', values: ['foo', 'bar', 'world'], synchronize: true }; - expect(compareEnums.onCompare(source, target)).toEqual([ + expect(compareEnums().onCompare(source, target)).toEqual([ { enumName: 'test', type: 'EnumDrop', diff --git a/server/src/sql-tools/comparers/enum.comparer.ts b/server/src/sql-tools/comparers/enum.comparer.ts index d81f9ed3c0..efc08ae727 100644 --- a/server/src/sql-tools/comparers/enum.comparer.ts +++ b/server/src/sql-tools/comparers/enum.comparer.ts @@ -1,6 +1,6 @@ import { Comparer, DatabaseEnum, Reason } from 'src/sql-tools/types'; -export const compareEnums: Comparer = { +export const compareEnums = (): Comparer => ({ onMissing: (source) => [ { type: 'EnumCreate', @@ -35,4 +35,4 @@ export const compareEnums: Comparer = { return []; }, -}; +}); diff --git a/server/src/sql-tools/comparers/extension.comparer.spec.ts b/server/src/sql-tools/comparers/extension.comparer.spec.ts index 38e553719d..df70ccc761 100644 --- a/server/src/sql-tools/comparers/extension.comparer.spec.ts +++ b/server/src/sql-tools/comparers/extension.comparer.spec.ts @@ -7,7 +7,7 @@ const testExtension = { name: 'test', synchronize: true }; describe('compareExtensions', () => { describe('onExtra', () => { it('should work', () => { - expect(compareExtensions.onExtra(testExtension)).toEqual([ + expect(compareExtensions().onExtra(testExtension)).toEqual([ { extensionName: 'test', type: 'ExtensionDrop', @@ -19,7 +19,7 @@ describe('compareExtensions', () => { describe('onMissing', () => { it('should work', () => { - expect(compareExtensions.onMissing(testExtension)).toEqual([ + expect(compareExtensions().onMissing(testExtension)).toEqual([ { type: 'ExtensionCreate', extension: testExtension, @@ -31,7 +31,7 @@ describe('compareExtensions', () => { describe('onCompare', () => { it('should work', () => { - expect(compareExtensions.onCompare(testExtension, testExtension)).toEqual([]); + expect(compareExtensions().onCompare(testExtension, testExtension)).toEqual([]); }); }); }); diff --git a/server/src/sql-tools/comparers/extension.comparer.ts b/server/src/sql-tools/comparers/extension.comparer.ts index 441b00e3e3..3cb70dadc4 100644 --- a/server/src/sql-tools/comparers/extension.comparer.ts +++ b/server/src/sql-tools/comparers/extension.comparer.ts @@ -1,6 +1,6 @@ import { Comparer, DatabaseExtension, Reason } from 'src/sql-tools/types'; -export const compareExtensions: Comparer = { +export const compareExtensions = (): Comparer => ({ onMissing: (source) => [ { type: 'ExtensionCreate', @@ -19,4 +19,4 @@ export const compareExtensions: Comparer = { // if the name matches they are the same return []; }, -}; +}); diff --git a/server/src/sql-tools/comparers/function.comparer.spec.ts b/server/src/sql-tools/comparers/function.comparer.spec.ts index 964768cf98..3d18aaf50a 100644 --- a/server/src/sql-tools/comparers/function.comparer.spec.ts +++ b/server/src/sql-tools/comparers/function.comparer.spec.ts @@ -11,7 +11,7 @@ const testFunction: DatabaseFunction = { describe('compareFunctions', () => { describe('onExtra', () => { it('should work', () => { - expect(compareFunctions.onExtra(testFunction)).toEqual([ + expect(compareFunctions().onExtra(testFunction)).toEqual([ { functionName: 'test', type: 'FunctionDrop', @@ -23,7 +23,7 @@ describe('compareFunctions', () => { describe('onMissing', () => { it('should work', () => { - expect(compareFunctions.onMissing(testFunction)).toEqual([ + expect(compareFunctions().onMissing(testFunction)).toEqual([ { type: 'FunctionCreate', function: testFunction, @@ -35,13 +35,13 @@ describe('compareFunctions', () => { describe('onCompare', () => { it('should ignore functions with the same hash', () => { - expect(compareFunctions.onCompare(testFunction, testFunction)).toEqual([]); + expect(compareFunctions().onCompare(testFunction, testFunction)).toEqual([]); }); it('should report differences if functions have different hashes', () => { const source: DatabaseFunction = { ...testFunction, expression: 'SELECT 1' }; const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' }; - expect(compareFunctions.onCompare(source, target)).toEqual([ + expect(compareFunctions().onCompare(source, target)).toEqual([ { type: 'FunctionCreate', reason: 'function expression has changed (SELECT 1 vs SELECT 2)', diff --git a/server/src/sql-tools/comparers/function.comparer.ts b/server/src/sql-tools/comparers/function.comparer.ts index 000cf07058..c6217ee708 100644 --- a/server/src/sql-tools/comparers/function.comparer.ts +++ b/server/src/sql-tools/comparers/function.comparer.ts @@ -1,6 +1,6 @@ import { Comparer, DatabaseFunction, Reason } from 'src/sql-tools/types'; -export const compareFunctions: Comparer = { +export const compareFunctions = (): Comparer => ({ onMissing: (source) => [ { type: 'FunctionCreate', @@ -29,4 +29,4 @@ export const compareFunctions: Comparer = { return []; }, -}; +}); diff --git a/server/src/sql-tools/comparers/index.comparer.spec.ts b/server/src/sql-tools/comparers/index.comparer.spec.ts index b00be386e0..9ae7f34f04 100644 --- a/server/src/sql-tools/comparers/index.comparer.spec.ts +++ b/server/src/sql-tools/comparers/index.comparer.spec.ts @@ -13,7 +13,7 @@ const testIndex: DatabaseIndex = { describe('compareIndexes', () => { describe('onExtra', () => { it('should work', () => { - expect(compareIndexes.onExtra(testIndex)).toEqual([ + expect(compareIndexes().onExtra(testIndex)).toEqual([ { type: 'IndexDrop', indexName: 'test', @@ -25,7 +25,7 @@ describe('compareIndexes', () => { describe('onMissing', () => { it('should work', () => { - expect(compareIndexes.onMissing(testIndex)).toEqual([ + expect(compareIndexes().onMissing(testIndex)).toEqual([ { type: 'IndexCreate', index: testIndex, @@ -37,7 +37,7 @@ describe('compareIndexes', () => { describe('onCompare', () => { it('should work', () => { - expect(compareIndexes.onCompare(testIndex, testIndex)).toEqual([]); + expect(compareIndexes().onCompare(testIndex, testIndex)).toEqual([]); }); it('should drop and recreate when column list is different', () => { @@ -55,7 +55,7 @@ describe('compareIndexes', () => { unique: true, synchronize: true, }; - expect(compareIndexes.onCompare(source, target)).toEqual([ + expect(compareIndexes().onCompare(source, target)).toEqual([ { indexName: 'test', type: 'IndexDrop', diff --git a/server/src/sql-tools/comparers/index.comparer.ts b/server/src/sql-tools/comparers/index.comparer.ts index a3db9a61e0..e474302c6e 100644 --- a/server/src/sql-tools/comparers/index.comparer.ts +++ b/server/src/sql-tools/comparers/index.comparer.ts @@ -1,7 +1,7 @@ import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types'; -export const compareIndexes: Comparer = { +export const compareIndexes = (): Comparer => ({ getRenameKey: (index) => { if (index.override) { return index.override.value.sql.replace(index.name, 'INDEX_NAME'); @@ -59,4 +59,4 @@ export const compareIndexes: Comparer = { return []; }, -}; +}); diff --git a/server/src/sql-tools/comparers/override.comparer.spec.ts b/server/src/sql-tools/comparers/override.comparer.spec.ts index 22093381ff..dfa6fa4455 100644 --- a/server/src/sql-tools/comparers/override.comparer.spec.ts +++ b/server/src/sql-tools/comparers/override.comparer.spec.ts @@ -11,7 +11,7 @@ const testOverride: DatabaseOverride = { describe('compareOverrides', () => { describe('onExtra', () => { it('should work', () => { - expect(compareOverrides.onExtra(testOverride)).toEqual([ + expect(compareOverrides().onExtra(testOverride)).toEqual([ { type: 'OverrideDrop', overrideName: 'test', @@ -23,7 +23,7 @@ describe('compareOverrides', () => { describe('onMissing', () => { it('should work', () => { - expect(compareOverrides.onMissing(testOverride)).toEqual([ + expect(compareOverrides().onMissing(testOverride)).toEqual([ { type: 'OverrideCreate', override: testOverride, @@ -35,7 +35,7 @@ describe('compareOverrides', () => { describe('onCompare', () => { it('should work', () => { - expect(compareOverrides.onCompare(testOverride, testOverride)).toEqual([]); + expect(compareOverrides().onCompare(testOverride, testOverride)).toEqual([]); }); it('should drop and recreate when the value changes', () => { @@ -57,7 +57,7 @@ describe('compareOverrides', () => { }, synchronize: true, }; - expect(compareOverrides.onCompare(source, target)).toEqual([ + expect(compareOverrides().onCompare(source, target)).toEqual([ { override: source, type: 'OverrideUpdate', diff --git a/server/src/sql-tools/comparers/override.comparer.ts b/server/src/sql-tools/comparers/override.comparer.ts index 369f7cd59f..999770bf69 100644 --- a/server/src/sql-tools/comparers/override.comparer.ts +++ b/server/src/sql-tools/comparers/override.comparer.ts @@ -1,6 +1,6 @@ import { Comparer, DatabaseOverride, Reason } from 'src/sql-tools/types'; -export const compareOverrides: Comparer = { +export const compareOverrides = (): Comparer => ({ onMissing: (source) => [ { type: 'OverrideCreate', @@ -26,4 +26,4 @@ export const compareOverrides: Comparer = { return []; }, -}; +}); diff --git a/server/src/sql-tools/comparers/parameter.comparer.spec.ts b/server/src/sql-tools/comparers/parameter.comparer.spec.ts index cd1520faff..23e6c78118 100644 --- a/server/src/sql-tools/comparers/parameter.comparer.spec.ts +++ b/server/src/sql-tools/comparers/parameter.comparer.spec.ts @@ -13,7 +13,7 @@ const testParameter: DatabaseParameter = { describe('compareParameters', () => { describe('onExtra', () => { it('should work', () => { - expect(compareParameters.onExtra(testParameter)).toEqual([ + expect(compareParameters().onExtra(testParameter)).toEqual([ { type: 'ParameterReset', databaseName: 'immich', @@ -26,7 +26,7 @@ describe('compareParameters', () => { describe('onMissing', () => { it('should work', () => { - expect(compareParameters.onMissing(testParameter)).toEqual([ + expect(compareParameters().onMissing(testParameter)).toEqual([ { type: 'ParameterSet', parameter: testParameter, @@ -38,7 +38,7 @@ describe('compareParameters', () => { describe('onCompare', () => { it('should work', () => { - expect(compareParameters.onCompare(testParameter, testParameter)).toEqual([]); + expect(compareParameters().onCompare(testParameter, testParameter)).toEqual([]); }); }); }); diff --git a/server/src/sql-tools/comparers/parameter.comparer.ts b/server/src/sql-tools/comparers/parameter.comparer.ts index d1a33ad090..41d0508d70 100644 --- a/server/src/sql-tools/comparers/parameter.comparer.ts +++ b/server/src/sql-tools/comparers/parameter.comparer.ts @@ -1,6 +1,6 @@ import { Comparer, DatabaseParameter, Reason } from 'src/sql-tools/types'; -export const compareParameters: Comparer = { +export const compareParameters = (): Comparer => ({ onMissing: (source) => [ { type: 'ParameterSet', @@ -20,4 +20,4 @@ export const compareParameters: Comparer = { // TODO return []; }, -}; +}); diff --git a/server/src/sql-tools/comparers/table.comparer.spec.ts b/server/src/sql-tools/comparers/table.comparer.spec.ts index 575e25ab44..909db26ea9 100644 --- a/server/src/sql-tools/comparers/table.comparer.spec.ts +++ b/server/src/sql-tools/comparers/table.comparer.spec.ts @@ -14,7 +14,7 @@ const testTable: DatabaseTable = { describe('compareParameters', () => { describe('onExtra', () => { it('should work', () => { - expect(compareTables.onExtra(testTable)).toEqual([ + expect(compareTables({}).onExtra(testTable)).toEqual([ { type: 'TableDrop', tableName: 'test', @@ -26,7 +26,7 @@ describe('compareParameters', () => { describe('onMissing', () => { it('should work', () => { - expect(compareTables.onMissing(testTable)).toEqual([ + expect(compareTables({}).onMissing(testTable)).toEqual([ { type: 'TableCreate', table: testTable, @@ -38,7 +38,7 @@ describe('compareParameters', () => { describe('onCompare', () => { it('should work', () => { - expect(compareTables.onCompare(testTable, testTable)).toEqual([]); + expect(compareTables({}).onCompare(testTable, testTable)).toEqual([]); }); }); }); diff --git a/server/src/sql-tools/comparers/table.comparer.ts b/server/src/sql-tools/comparers/table.comparer.ts index 0b36b7fce4..6576dce1b1 100644 --- a/server/src/sql-tools/comparers/table.comparer.ts +++ b/server/src/sql-tools/comparers/table.comparer.ts @@ -3,9 +3,9 @@ import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer' import { compareIndexes } from 'src/sql-tools/comparers/index.comparer'; import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer'; import { compare } from 'src/sql-tools/helpers'; -import { Comparer, DatabaseTable, Reason, SchemaDiff } from 'src/sql-tools/types'; +import { Comparer, DatabaseTable, Reason, SchemaDiffOptions } from 'src/sql-tools/types'; -export const compareTables: Comparer = { +export const compareTables = (options: SchemaDiffOptions): Comparer => ({ onMissing: (source) => [ { type: 'TableCreate', @@ -20,14 +20,12 @@ export const compareTables: Comparer = { reason: Reason.MissingInSource, }, ], - onCompare: (source, target) => compareTable(source, target), -}; - -const compareTable = (source: DatabaseTable, target: DatabaseTable): SchemaDiff[] => { - return [ - ...compare(source.columns, target.columns, {}, compareColumns), - ...compare(source.indexes, target.indexes, {}, compareIndexes), - ...compare(source.constraints, target.constraints, {}, compareConstraints), - ...compare(source.triggers, target.triggers, {}, compareTriggers), - ]; -}; + onCompare: (source, target) => { + return [ + ...compare(source.columns, target.columns, options.columns, compareColumns()), + ...compare(source.indexes, target.indexes, options.indexes, compareIndexes()), + ...compare(source.constraints, target.constraints, options.constraints, compareConstraints()), + ...compare(source.triggers, target.triggers, options.triggers, compareTriggers()), + ]; + }, +}); diff --git a/server/src/sql-tools/comparers/trigger.comparer.spec.ts b/server/src/sql-tools/comparers/trigger.comparer.spec.ts index 731fae8da2..c80b0d2273 100644 --- a/server/src/sql-tools/comparers/trigger.comparer.spec.ts +++ b/server/src/sql-tools/comparers/trigger.comparer.spec.ts @@ -15,7 +15,7 @@ const testTrigger: DatabaseTrigger = { describe('compareTriggers', () => { describe('onExtra', () => { it('should work', () => { - expect(compareTriggers.onExtra(testTrigger)).toEqual([ + expect(compareTriggers().onExtra(testTrigger)).toEqual([ { type: 'TriggerDrop', tableName: 'table1', @@ -28,7 +28,7 @@ describe('compareTriggers', () => { describe('onMissing', () => { it('should work', () => { - expect(compareTriggers.onMissing(testTrigger)).toEqual([ + expect(compareTriggers().onMissing(testTrigger)).toEqual([ { type: 'TriggerCreate', trigger: testTrigger, @@ -40,49 +40,49 @@ describe('compareTriggers', () => { describe('onCompare', () => { it('should work', () => { - expect(compareTriggers.onCompare(testTrigger, testTrigger)).toEqual([]); + expect(compareTriggers().onCompare(testTrigger, testTrigger)).toEqual([]); }); it('should detect a change in function name', () => { const source: DatabaseTrigger = { ...testTrigger, functionName: 'my_new_name' }; const target: DatabaseTrigger = { ...testTrigger, functionName: 'my_old_name' }; const reason = `function is different (my_new_name vs my_old_name)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); + expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in actions', () => { const source: DatabaseTrigger = { ...testTrigger, actions: ['delete'] }; const target: DatabaseTrigger = { ...testTrigger, actions: ['delete', 'insert'] }; const reason = `action is different (delete vs delete,insert)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); + expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in timing', () => { const source: DatabaseTrigger = { ...testTrigger, timing: 'before' }; const target: DatabaseTrigger = { ...testTrigger, timing: 'after' }; const reason = `timing method is different (before vs after)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); + expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in scope', () => { const source: DatabaseTrigger = { ...testTrigger, scope: 'row' }; const target: DatabaseTrigger = { ...testTrigger, scope: 'statement' }; const reason = `scope is different (row vs statement)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); + expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in new table reference', () => { const source: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: 'new_table' }; const target: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: undefined }; const reason = `new table reference is different (new_table vs undefined)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); + expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); it('should detect a change in old table reference', () => { const source: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: 'old_table' }; const target: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: undefined }; const reason = `old table reference is different (old_table vs undefined)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); + expect(compareTriggers().onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); }); }); }); diff --git a/server/src/sql-tools/comparers/trigger.comparer.ts b/server/src/sql-tools/comparers/trigger.comparer.ts index da1de6e48b..4ba2d5dba3 100644 --- a/server/src/sql-tools/comparers/trigger.comparer.ts +++ b/server/src/sql-tools/comparers/trigger.comparer.ts @@ -1,6 +1,6 @@ import { Comparer, DatabaseTrigger, Reason } from 'src/sql-tools/types'; -export const compareTriggers: Comparer = { +export const compareTriggers = (): Comparer => ({ onMissing: (source) => [ { type: 'TriggerCreate', @@ -38,4 +38,4 @@ export const compareTriggers: Comparer = { return []; }, -}; +}); diff --git a/server/src/sql-tools/schema-diff.ts b/server/src/sql-tools/schema-diff.ts index bca58c3228..846210931b 100644 --- a/server/src/sql-tools/schema-diff.ts +++ b/server/src/sql-tools/schema-diff.ts @@ -20,12 +20,12 @@ import { */ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => { const items = [ - ...compare(source.parameters, target.parameters, options.parameters, compareParameters), - ...compare(source.extensions, target.extensions, options.extensions, compareExtensions), - ...compare(source.functions, target.functions, options.functions, compareFunctions), - ...compare(source.enums, target.enums, options.enums, compareEnums), - ...compare(source.tables, target.tables, options.tables, compareTables), - ...compare(source.overrides, target.overrides, options.overrides, compareOverrides), + ...compare(source.parameters, target.parameters, options.parameters, compareParameters()), + ...compare(source.extensions, target.extensions, options.extensions, compareExtensions()), + ...compare(source.functions, target.functions, options.functions, compareFunctions()), + ...compare(source.enums, target.enums, options.enums, compareEnums()), + ...compare(source.tables, target.tables, options.tables, compareTables(options)), + ...compare(source.overrides, target.overrides, options.overrides, compareOverrides()), ]; type SchemaName = SchemaDiff['type']; @@ -103,6 +103,7 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio return { items: orderedItems, asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options), + asHuman: () => schemaDiffToHuman(orderedItems), }; }; @@ -113,7 +114,14 @@ export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOpt return items.flatMap((item) => asSql(item, options)); }; -const asSql = (item: SchemaDiff, options: SchemaDiffToSqlOptions): string[] => { +/** + * Convert schema diff into human readable statements + */ +export const schemaDiffToHuman = (items: SchemaDiff[]): string[] => { + return items.flatMap((item) => asHuman(item)); +}; + +export const asSql = (item: SchemaDiff, options: SchemaDiffToSqlOptions): string[] => { const ctx = new BaseContext(options); for (const transform of transformers) { const result = transform(ctx, item); @@ -127,6 +135,88 @@ const asSql = (item: SchemaDiff, options: SchemaDiffToSqlOptions): string[] => { throw new Error(`Unhandled schema diff type: ${item.type}`); }; +export const asHuman = (item: SchemaDiff): string => { + switch (item.type) { + case 'ExtensionCreate': { + return `The extension "${item.extension.name}" is missing and needs to be created`; + } + case 'ExtensionDrop': { + return `The extension "${item.extensionName}" exists but is no longer needed`; + } + case 'FunctionCreate': { + return `The function "${item.function.name}" is missing and needs to be created`; + } + case 'FunctionDrop': { + return `The function "${item.functionName}" exists but should be removed`; + } + case 'TableCreate': { + return `The table "${item.table.name}" is missing and needs to be created`; + } + case 'TableDrop': { + return `The table "${item.tableName}" exists but should be removed`; + } + case 'ColumnAdd': { + return `The column "${item.column.tableName}"."${item.column.name}" is missing and needs to be created`; + } + case 'ColumnRename': { + return `The column "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`; + } + case 'ColumnAlter': { + return `The column "${item.tableName}"."${item.columnName}" has changes that need to be applied ${JSON.stringify( + item.changes, + )}`; + } + case 'ColumnDrop': { + return `The column "${item.tableName}"."${item.columnName}" exists but should be removed`; + } + case 'ConstraintAdd': { + return `The constraint "${item.constraint.tableName}"."${item.constraint.name}" (${item.constraint.type}) is missing and needs to be created`; + } + case 'ConstraintRename': { + return `The constraint "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`; + } + case 'ConstraintDrop': { + return `The constraint "${item.tableName}"."${item.constraintName}" exists but should be removed`; + } + case 'IndexCreate': { + return `The index "${item.index.tableName}"."${item.index.name}" is missing and needs to be created`; + } + case 'IndexRename': { + return `The index "${item.tableName}"."${item.oldName}" was renamed to "${item.tableName}"."${item.newName}"`; + } + case 'IndexDrop': { + return `The index "${item.indexName}" exists but is no longer needed`; + } + case 'TriggerCreate': { + return `The trigger "${item.trigger.tableName}"."${item.trigger.name}" is missing and needs to be created`; + } + case 'TriggerDrop': { + return `The trigger "${item.tableName}"."${item.triggerName}" exists but is no longer needed`; + } + case 'ParameterSet': { + return `The configuration parameter "${item.parameter.name}" has a different value and needs to be updated to "${item.parameter.value}"`; + } + case 'ParameterReset': { + return `The configuration parameter "${item.parameterName}" is set, but should be reset to the default value`; + } + case 'EnumCreate': { + return `The enum "${item.enum.name}" is missing and needs to be created`; + } + case 'EnumDrop': { + return `The enum "${item.enumName}" exists but is no longer needed`; + } + case 'OverrideCreate': { + return `The override "${item.override.name}" is missing and needs to be created`; + } + case 'OverrideUpdate': { + return `The override "${item.override.name}" needs to be updated`; + } + case 'OverrideDrop': { + return `The override "${item.overrideName}" exists but is no longer needed`; + } + } +}; + const withComments = (comments: boolean | undefined, item: SchemaDiff): string => { if (!comments) { return ''; diff --git a/server/src/sql-tools/schema-from-database.ts b/server/src/sql-tools/schema-from-database.ts index b7b76a68b1..ee34e9dd8d 100644 --- a/server/src/sql-tools/schema-from-database.ts +++ b/server/src/sql-tools/schema-from-database.ts @@ -5,14 +5,20 @@ import { ReaderContext } from 'src/sql-tools/contexts/reader-context'; import { readers } from 'src/sql-tools/readers'; import { DatabaseSchema, PostgresDB, SchemaFromDatabaseOptions } from 'src/sql-tools/types'; +export type DatabaseLike = Sql | Kysely; + +const isKysely = (db: DatabaseLike): db is Kysely => db instanceof Kysely; + /** * Load schema from a database url */ export const schemaFromDatabase = async ( - postgres: Sql, + database: DatabaseLike, options: SchemaFromDatabaseOptions = {}, ): Promise => { - const db = new Kysely({ dialect: new PostgresJSDialect({ postgres }) }); + const db = isKysely(database) + ? (database as Kysely) + : new Kysely({ dialect: new PostgresJSDialect({ postgres: database }) }); const ctx = new ReaderContext(options); try { @@ -22,6 +28,9 @@ export const schemaFromDatabase = async ( return ctx.build(); } finally { - await db.destroy(); + // only close the connection it we created it + if (!isKysely(database)) { + await db.destroy(); + } } }; diff --git a/server/src/sql-tools/types.ts b/server/src/sql-tools/types.ts index 899ba1b963..9d93a79ff1 100644 --- a/server/src/sql-tools/types.ts +++ b/server/src/sql-tools/types.ts @@ -30,6 +30,10 @@ export type SchemaDiffToSqlOptions = BaseContextOptions & { export type SchemaDiffOptions = BaseContextOptions & { tables?: IgnoreOptions; + columns?: IgnoreOptions; + indexes?: IgnoreOptions; + triggers?: IgnoreOptions; + constraints?: IgnoreOptions; functions?: IgnoreOptions; enums?: IgnoreOptions; extensions?: IgnoreOptions; diff --git a/server/src/utils/database-backups.ts b/server/src/utils/database-backups.ts index 1d508e2a7d..70bedb32b1 100644 --- a/server/src/utils/database-backups.ts +++ b/server/src/utils/database-backups.ts @@ -1,20 +1,3 @@ -import { BadRequestException } from '@nestjs/common'; -import { debounce } from 'lodash'; -import { DateTime } from 'luxon'; -import path, { basename, join } from 'node:path'; -import { PassThrough, Readable, Writable } from 'node:stream'; -import { pipeline } from 'node:stream/promises'; -import semver from 'semver'; -import { serverVersion } from 'src/constants'; -import { StorageCore } from 'src/cores/storage.core'; -import { CacheControl, StorageFolder } from 'src/enum'; -import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; -import { ConfigRepository } from 'src/repositories/config.repository'; -import { DatabaseRepository } from 'src/repositories/database.repository'; -import { LoggingRepository } from 'src/repositories/logging.repository'; -import { ProcessRepository } from 'src/repositories/process.repository'; -import { StorageRepository } from 'src/repositories/storage.repository'; - export function isValidDatabaseBackupName(filename: string) { return filename.match(/^[\d\w-.]+\.sql(?:\.gz)?$/); } @@ -30,453 +13,12 @@ export function isFailedDatabaseBackupName(filename: string) { return filename.match(/^immich-db-backup-.*\.sql\.gz\.tmp$/); } -export function findVersion(filename: string) { +export function findDatabaseBackupVersion(filename: string) { return /-v(.*)-/.exec(filename)?.[1]; } -type BackupRepos = { - logger: LoggingRepository; - storage: StorageRepository; - config: ConfigRepository; - process: ProcessRepository; - database: DatabaseRepository; - health: MaintenanceHealthRepository; -}; - export class UnsupportedPostgresError extends Error { constructor(databaseVersion: string) { super(`Unsupported PostgreSQL version: ${databaseVersion}`); } } - -export async function buildPostgresLaunchArguments( - { logger, config, database }: Pick, - bin: 'pg_dump' | 'pg_dumpall' | 'psql', - options: { - singleTransaction?: boolean; - username?: string; - } = {}, -): Promise<{ - bin: string; - args: string[]; - databaseUsername: string; - databasePassword: string; - databaseVersion: string; - databaseMajorVersion?: number; -}> { - const { - database: { config: databaseConfig }, - } = config.getEnv(); - const isUrlConnection = databaseConfig.connectionType === 'url'; - - const databaseVersion = await database.getPostgresVersion(); - const databaseSemver = semver.coerce(databaseVersion); - const databaseMajorVersion = databaseSemver?.major; - - const args: string[] = []; - let databaseUsername; - - if (isUrlConnection) { - if (bin !== 'pg_dump') { - args.push('--dbname'); - } - - let url = databaseConfig.url; - if (URL.canParse(databaseConfig.url)) { - const parsedUrl = new URL(databaseConfig.url); - // remove known bad parameters - parsedUrl.searchParams.delete('uselibpqcompat'); - - databaseUsername = parsedUrl.username; - url = parsedUrl.toString(); - } - - // assume typical values if we can't parse URL or not present - databaseUsername ??= 'postgres'; - - args.push(url); - } else { - databaseUsername = databaseConfig.username; - - args.push('--username', databaseUsername, '--host', databaseConfig.host, '--port', databaseConfig.port.toString()); - - switch (bin) { - case 'pg_dumpall': { - args.push('--database'); - break; - } - case 'psql': { - args.push('--dbname'); - break; - } - } - - args.push(databaseConfig.database); - } - - switch (bin) { - case 'pg_dump': - case 'pg_dumpall': { - args.push('--clean', '--if-exists'); - break; - } - case 'psql': { - if (options.singleTransaction) { - args.push( - // don't commit any transaction on failure - '--single-transaction', - // exit with non-zero code on error - '--set', - 'ON_ERROR_STOP=on', - ); - } - - args.push( - // used for progress monitoring - '--echo-all', - '--output=/dev/null', - ); - break; - } - } - - if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <19.0.0')) { - logger.error(`Database Restore Failure: Unsupported PostgreSQL version: ${databaseVersion}`); - throw new UnsupportedPostgresError(databaseVersion); - } - - return { - bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`, - args, - databaseUsername, - databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password, - databaseVersion, - databaseMajorVersion, - }; -} - -export async function createDatabaseBackup( - { logger, storage, process: processRepository, ...pgRepos }: Omit, - filenamePrefix: string = '', -): Promise { - logger.debug(`Database Backup Started`); - - const { bin, args, databasePassword, databaseVersion, databaseMajorVersion } = await buildPostgresLaunchArguments( - { logger, ...pgRepos }, - 'pg_dump', - ); - - logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); - - const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`; - const backupFilePath = join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); - const temporaryFilePath = `${backupFilePath}.tmp`; - - try { - const pgdump = processRepository.spawnDuplexStream(bin, args, { - env: { - PATH: process.env.PATH, - PGPASSWORD: databasePassword, - }, - }); - - const gzip = processRepository.spawnDuplexStream('gzip', ['--rsyncable']); - const fileStream = storage.createWriteStream(temporaryFilePath); - - await pipeline(pgdump, gzip, fileStream); - await storage.rename(temporaryFilePath, backupFilePath); - } catch (error) { - logger.error(`Database Backup Failure: ${error}`); - await storage - .unlink(temporaryFilePath) - .catch((error) => logger.error(`Failed to delete failed backup file: ${error}`)); - throw error; - } - - logger.log(`Database Backup Success`); - return backupFilePath; -} - -const SQL_DROP_CONNECTIONS = ` - -- drop all other database connections - SELECT pg_terminate_backend(pid) - FROM pg_stat_activity - WHERE datname = current_database() - AND pid <> pg_backend_pid(); -`; - -const SQL_RESET_SCHEMA = (username: string) => ` - -- re-create the default schema - DROP SCHEMA public CASCADE; - CREATE SCHEMA public; - - -- restore access to schema - GRANT ALL ON SCHEMA public TO "${username}"; - GRANT ALL ON SCHEMA public TO public; -`; - -async function* sql(inputStream: Readable, databaseUsername: string, isPgClusterDump: boolean) { - yield SQL_DROP_CONNECTIONS; - yield isPgClusterDump - ? // it is likely the dump contains SQL to try to drop the currently active - // database to ensure we have a fresh slate; if the `postgres` database exists - // then prefer to switch before continuing otherwise this will just silently fail - String.raw` - \c postgres - ` - : SQL_RESET_SCHEMA(databaseUsername); - - for await (const chunk of inputStream) { - yield chunk; - } -} - -async function* sqlRollback(inputStream: Readable, databaseUsername: string) { - yield SQL_DROP_CONNECTIONS; - yield SQL_RESET_SCHEMA(databaseUsername); - - for await (const chunk of inputStream) { - yield chunk; - } -} - -export async function restoreDatabaseBackup( - { logger, storage, process: processRepository, database: databaseRepository, health, ...pgRepos }: BackupRepos, - filename: string, - progressCb?: (action: 'backup' | 'restore' | 'migrations' | 'rollback', progress: number) => void, -): Promise { - logger.debug(`Database Restore Started`); - - let complete = false; - try { - if (!isValidDatabaseBackupName(filename)) { - throw new Error('Invalid backup file format!'); - } - - const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); - await storage.stat(backupFilePath); // => check file exists - - let isPgClusterDump = false; - const version = findVersion(filename); - if (version && semver.satisfies(version, '<= 2.4')) { - isPgClusterDump = true; - } - - const { bin, args, databaseUsername, databasePassword, databaseMajorVersion } = await buildPostgresLaunchArguments( - { logger, database: databaseRepository, ...pgRepos }, - 'psql', - { - singleTransaction: !isPgClusterDump, - }, - ); - - progressCb?.('backup', 0.05); - - const restorePointFilePath = await createDatabaseBackup( - { logger, storage, process: processRepository, database: databaseRepository, ...pgRepos }, - 'restore-point-', - ); - - logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`); - - let inputStream: Readable; - if (backupFilePath.endsWith('.gz')) { - const fileStream = storage.createPlainReadStream(backupFilePath); - const gunzip = storage.createGunzip(); - fileStream.pipe(gunzip); - inputStream = gunzip; - } else { - inputStream = storage.createPlainReadStream(backupFilePath); - } - - const sqlStream = Readable.from(sql(inputStream, databaseUsername, isPgClusterDump)); - const psql = processRepository.spawnDuplexStream(bin, args, { - env: { - PATH: process.env.PATH, - PGPASSWORD: databasePassword, - }, - }); - - const [progressSource, progressSink] = createSqlProgressStreams((progress) => { - if (complete) { - return; - } - - logger.log(`Restore progress ~ ${(progress * 100).toFixed(2)}%`); - progressCb?.('restore', progress); - }); - - await pipeline(sqlStream, progressSource, psql, progressSink); - - try { - progressCb?.('migrations', 0.9); - await databaseRepository.runMigrations(); - await health.checkApiHealth(); - } catch (error) { - progressCb?.('rollback', 0); - - const fileStream = storage.createPlainReadStream(restorePointFilePath); - const gunzip = storage.createGunzip(); - fileStream.pipe(gunzip); - inputStream = gunzip; - - const sqlStream = Readable.from(sqlRollback(inputStream, databaseUsername)); - const psql = processRepository.spawnDuplexStream(bin, args, { - env: { - PATH: process.env.PATH, - PGPASSWORD: databasePassword, - }, - }); - - const [progressSource, progressSink] = createSqlProgressStreams((progress) => { - if (complete) { - return; - } - - logger.log(`Rollback progress ~ ${(progress * 100).toFixed(2)}%`); - progressCb?.('rollback', progress); - }); - - await pipeline(sqlStream, progressSource, psql, progressSink); - - throw error; - } - } catch (error) { - logger.error(`Database Restore Failure: ${error}`); - throw error; - } finally { - complete = true; - } - - logger.log(`Database Restore Success`); -} - -export async function deleteDatabaseBackup({ storage }: Pick, files: string[]): Promise { - const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); - - if (files.some((filename) => !isValidDatabaseBackupName(filename))) { - throw new BadRequestException('Invalid backup name!'); - } - - await Promise.all(files.map((filename) => storage.unlink(path.join(backupsFolder, filename)))); -} - -export async function listDatabaseBackups({ - storage, -}: Pick): Promise<{ filename: string; filesize: number }[]> { - const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); - const files = await storage.readdir(backupsFolder); - - const validFiles = files - .filter((fn) => isValidDatabaseBackupName(fn)) - .toSorted((a, b) => (a.startsWith('uploaded-') === b.startsWith('uploaded-') ? a.localeCompare(b) : 1)) - .toReversed(); - - const backups = await Promise.all( - validFiles.map(async (filename) => { - const stats = await storage.stat(path.join(backupsFolder, filename)); - return { filename, filesize: stats.size }; - }), - ); - - return backups; -} - -export async function uploadDatabaseBackup( - { storage }: Pick, - file: Express.Multer.File, -): Promise { - const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); - const fn = basename(file.originalname); - if (!isValidDatabaseBackupName(fn)) { - throw new BadRequestException('Invalid backup name!'); - } - - const path = join(backupsFolder, `uploaded-${fn}`); - await storage.createOrOverwriteFile(path, file.buffer); -} - -export function downloadDatabaseBackup(fileName: string) { - if (!isValidDatabaseBackupName(fileName)) { - throw new BadRequestException('Invalid backup name!'); - } - - const path = join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName); - - return { - path, - fileName, - cacheControl: CacheControl.PrivateWithoutCache, - contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql', - }; -} - -function createSqlProgressStreams(cb: (progress: number) => void) { - const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin'); - const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`); - - let readingStdin = false; - let sequenceIdx = 0; - - let linesSent = 0; - let linesProcessed = 0; - - const startedAt = +Date.now(); - const cbDebounced = debounce( - () => { - const progress = source.writableEnded - ? Math.min(1, linesProcessed / linesSent) - : // progress simulation while we're in an indeterminate state - Math.min(0.3, 0.1 + (Date.now() - startedAt) / 1e4); - cb(progress); - }, - 100, - { - maxWait: 100, - }, - ); - - let lastByte = -1; - const source = new PassThrough({ - transform(chunk, _encoding, callback) { - for (const byte of chunk) { - if (!readingStdin && byte === 10 && lastByte !== 10) { - linesSent += 1; - } - - lastByte = byte; - - const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER; - if (sequence[sequenceIdx] === byte) { - sequenceIdx += 1; - - if (sequence.length === sequenceIdx) { - sequenceIdx = 0; - readingStdin = !readingStdin; - } - } else { - sequenceIdx = 0; - } - } - - cbDebounced(); - this.push(chunk); - callback(); - }, - }); - - const sink = new Writable({ - write(chunk, _encoding, callback) { - for (const byte of chunk) { - if (byte === 10) { - linesProcessed++; - } - } - - cbDebounced(); - callback(); - }, - }); - - return [source, sink]; -} diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index a041946a28..9ae15fd7d5 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -102,6 +102,10 @@ export const getKyselyConfig = ( }), log(event) { if (event.level === 'error') { + if (isAssetChecksumConstraint(event.error)) { + return; + } + console.error('Query failed :', { durationMs: event.queryDurationMillis, error: event.error, diff --git a/server/test/factories/asset-face.factory.ts b/server/test/factories/asset-face.factory.ts new file mode 100644 index 0000000000..899b529766 --- /dev/null +++ b/server/test/factories/asset-face.factory.ts @@ -0,0 +1,47 @@ +import { Selectable } from 'kysely'; +import { SourceType } from 'src/enum'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { build } from 'test/factories/builder.factory'; +import { PersonFactory } from 'test/factories/person.factory'; +import { AssetFaceLike, FactoryBuilder, PersonLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AssetFaceFactory { + #person: PersonFactory | null = null; + + private constructor(private readonly value: Selectable) {} + + static create(dto: AssetFaceLike = {}) { + return AssetFaceFactory.from(dto).build(); + } + + static from(dto: AssetFaceLike = {}) { + return new AssetFaceFactory({ + assetId: newUuid(), + boundingBoxX1: 11, + boundingBoxX2: 12, + boundingBoxY1: 21, + boundingBoxY2: 22, + deletedAt: null, + id: newUuid(), + imageHeight: 42, + imageWidth: 420, + isVisible: true, + personId: null, + sourceType: SourceType.MachineLearning, + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + person(dto: PersonLike = {}, builder?: FactoryBuilder) { + this.#person = build(PersonFactory.from(dto), builder); + this.value.personId = this.#person.build().id; + return this; + } + + build() { + return { ...this.value, person: this.#person?.build() ?? null }; + } +} diff --git a/server/test/factories/asset.factory.ts b/server/test/factories/asset.factory.ts index 8cbf704abf..258e2aff38 100644 --- a/server/test/factories/asset.factory.ts +++ b/server/test/factories/asset.factory.ts @@ -1,12 +1,23 @@ import { Selectable } from 'kysely'; import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; -import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetTable } from 'src/schema/tables/asset.table'; +import { StackTable } from 'src/schema/tables/stack.table'; import { AssetEditFactory } from 'test/factories/asset-edit.factory'; import { AssetExifFactory } from 'test/factories/asset-exif.factory'; +import { AssetFaceFactory } from 'test/factories/asset-face.factory'; import { AssetFileFactory } from 'test/factories/asset-file.factory'; import { build } from 'test/factories/builder.factory'; -import { AssetEditLike, AssetExifLike, AssetFileLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types'; +import { StackFactory } from 'test/factories/stack.factory'; +import { + AssetEditLike, + AssetExifLike, + AssetFaceLike, + AssetFileLike, + AssetLike, + FactoryBuilder, + StackLike, + UserLike, +} from 'test/factories/types'; import { UserFactory } from 'test/factories/user.factory'; import { newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory'; @@ -15,6 +26,8 @@ export class AssetFactory { #assetExif?: AssetExifFactory; #files: AssetFileFactory[] = []; #edits: AssetEditFactory[] = []; + #faces: AssetFaceFactory[] = []; + #stack?: Selectable & { assets: Selectable[]; primaryAsset: Selectable }; private constructor(private readonly value: Selectable) { value.ownerId ??= newUuid(); @@ -28,7 +41,7 @@ export class AssetFactory { static from(dto: AssetLike = {}) { const id = dto.id ?? newUuid(); - const originalFileName = dto.originalFileName ?? `IMG_${id}.jpg`; + const originalFileName = dto.originalFileName ?? (dto.type === AssetType.Video ? `MOV_${id}.mp4` : `IMG_${id}.jpg`); return new AssetFactory({ id, @@ -82,6 +95,11 @@ export class AssetFactory { return this; } + face(dto: AssetFaceLike = {}, builder?: FactoryBuilder) { + this.#faces.push(build(AssetFaceFactory.from(dto), builder)); + return this; + } + file(dto: AssetFileLike = {}, builder?: FactoryBuilder) { this.#files.push(build(AssetFileFactory.from(dto).asset(this.value), builder)); return this; @@ -111,6 +129,12 @@ export class AssetFactory { return this; } + stack(dto: StackLike = {}, builder?: FactoryBuilder) { + this.#stack = build(StackFactory.from(dto).primaryAsset(this.value), builder).build(); + this.value.stackId = this.#stack.id; + return this; + } + build() { const exif = this.#assetExif?.build(); @@ -120,7 +144,9 @@ export class AssetFactory { exifInfo: exif as NonNullable, files: this.#files.map((file) => file.build()), edits: this.#edits.map((edit) => edit.build()), - faces: [] as Selectable[], + faces: this.#faces.map((face) => face.build()), + stack: this.#stack ?? null, + tags: [], }; } } diff --git a/server/test/factories/person.factory.ts b/server/test/factories/person.factory.ts new file mode 100644 index 0000000000..8e016e5398 --- /dev/null +++ b/server/test/factories/person.factory.ts @@ -0,0 +1,34 @@ +import { Selectable } from 'kysely'; +import { PersonTable } from 'src/schema/tables/person.table'; +import { PersonLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class PersonFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: PersonLike = {}) { + return PersonFactory.from(dto).build(); + } + + static from(dto: PersonLike = {}) { + return new PersonFactory({ + birthDate: null, + color: null, + createdAt: newDate(), + faceAssetId: null, + id: newUuid(), + isFavorite: false, + isHidden: false, + name: 'person', + ownerId: newUuid(), + thumbnailPath: '/data/thumbs/person-thumbnail.jpg', + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/shared-link.factory.ts b/server/test/factories/shared-link.factory.ts index 585b43dd84..5ac5f1756b 100644 --- a/server/test/factories/shared-link.factory.ts +++ b/server/test/factories/shared-link.factory.ts @@ -2,14 +2,16 @@ import { Selectable } from 'kysely'; import { SharedLinkType } from 'src/enum'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; import { build } from 'test/factories/builder.factory'; -import { AlbumLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types'; +import { AlbumLike, AssetLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types'; import { UserFactory } from 'test/factories/user.factory'; import { factory, newDate, newUuid } from 'test/small.factory'; export class SharedLinkFactory { #owner: UserFactory; #album?: AlbumFactory; + #assets: AssetFactory[] = []; private constructor(private readonly value: Selectable) { value.userId ??= newUuid(); @@ -52,12 +54,18 @@ export class SharedLinkFactory { return this; } + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + const asset = build(AssetFactory.from(dto), builder); + this.#assets.push(asset); + return this; + } + build() { return { ...this.value, owner: this.#owner.build(), - album: this.#album?.build(), - assets: [], + album: this.#album?.build() ?? null, + assets: this.#assets.map((asset) => asset.build()), }; } } diff --git a/server/test/factories/stack.factory.ts b/server/test/factories/stack.factory.ts new file mode 100644 index 0000000000..69775973c4 --- /dev/null +++ b/server/test/factories/stack.factory.ts @@ -0,0 +1,52 @@ +import { Selectable } from 'kysely'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { build } from 'test/factories/builder.factory'; +import { AssetLike, FactoryBuilder, StackLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class StackFactory { + #assets: AssetFactory[] = []; + #primaryAsset: AssetFactory; + + private constructor(private readonly value: Selectable) { + this.#primaryAsset = AssetFactory.from(); + this.value.primaryAssetId = this.#primaryAsset.build().id; + } + + static create(dto: StackLike = {}) { + return StackFactory.from(dto).build(); + } + + static from(dto: StackLike = {}) { + return new StackFactory({ + createdAt: newDate(), + id: newUuid(), + ownerId: newUuid(), + primaryAssetId: newUuid(), + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + this.#assets.push(build(AssetFactory.from(dto), builder)); + return this; + } + + primaryAsset(dto: AssetLike = {}, builder?: FactoryBuilder) { + this.#primaryAsset = build(AssetFactory.from(dto), builder); + this.value.primaryAssetId = this.#primaryAsset.build().id; + this.#assets.push(this.#primaryAsset); + return this; + } + + build() { + return { + ...this.value, + assets: this.#assets.map((asset) => asset.build()), + primaryAsset: this.#primaryAsset.build(), + }; + } +} diff --git a/server/test/factories/types.ts b/server/test/factories/types.ts index 534e290f59..c5a327a624 100644 --- a/server/test/factories/types.ts +++ b/server/test/factories/types.ts @@ -3,9 +3,12 @@ import { AlbumUserTable } from 'src/schema/tables/album-user.table'; import { AlbumTable } from 'src/schema/tables/album.table'; import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetTable } from 'src/schema/tables/asset.table'; +import { PersonTable } from 'src/schema/tables/person.table'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { StackTable } from 'src/schema/tables/stack.table'; import { UserTable } from 'src/schema/tables/user.table'; export type FactoryBuilder = (builder: T) => R; @@ -18,3 +21,6 @@ export type AlbumLike = Partial>; export type AlbumUserLike = Partial>; export type SharedLinkLike = Partial>; export type UserLike = Partial>; +export type AssetFaceLike = Partial>; +export type PersonLike = Partial>; +export type StackLike = Partial>; diff --git a/server/test/factories/user.factory.ts b/server/test/factories/user.factory.ts index e6e84d94a1..125ce91e86 100644 --- a/server/test/factories/user.factory.ts +++ b/server/test/factories/user.factory.ts @@ -6,6 +6,8 @@ import { UserLike } from 'test/factories/types'; import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; export class UserFactory { + #metadata: Selectable[] = []; + private constructor(private value: Selectable) {} static create(dto: UserLike = {}) { @@ -37,10 +39,21 @@ export class UserFactory { }); } + metadata(dto: Partial> & Pick, 'key' | 'value'>) { + this.#metadata.push({ + updatedAt: newDate(), + updateId: newUuid(), + userId: newUuid(), + ...dto, + }); + + return this; + } + build() { return { ...this.value, - metadata: [] as UserMetadataTable[], + metadata: this.#metadata, }; } } diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts deleted file mode 100644 index 9480fdd5ab..0000000000 --- a/server/test/fixtures/album.stub.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { AlbumUserRole, AssetOrder } from 'src/enum'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { authStub } from 'test/fixtures/auth.stub'; -import { userStub } from 'test/fixtures/user.stub'; - -export const albumStub = { - empty: Object.freeze({ - id: 'album-1', - albumName: 'Empty album', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - sharedWithUser: Object.freeze({ - id: 'album-2', - albumName: 'Empty album shared with user', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [ - { - user: userStub.user1, - role: AlbumUserRole.Editor, - }, - ], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - oneAsset: Object.freeze({ - id: 'album-4', - albumName: 'Album with one asset', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [assetStub.image], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - emptyWithValidThumbnail: Object.freeze({ - id: 'album-5', - albumName: 'Empty album with valid thumbnail', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: assetStub.image, - albumThumbnailAssetId: assetStub.image.id, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), -}; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts deleted file mode 100644 index 3c89056f37..0000000000 --- a/server/test/fixtures/asset.stub.ts +++ /dev/null @@ -1,723 +0,0 @@ -import { AssetFace, AssetFile, Exif } from 'src/database'; -import { MapAsset } from 'src/dtos/asset-response.dto'; -import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto'; -import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; -import { StorageAsset } from 'src/types'; -import { authStub } from 'test/fixtures/auth.stub'; -import { fileStub } from 'test/fixtures/file.stub'; -import { userStub } from 'test/fixtures/user.stub'; -import { factory } from 'test/small.factory'; - -export const previewFile = factory.assetFile({ type: AssetFileType.Preview }); - -const thumbnailFile = factory.assetFile({ - type: AssetFileType.Thumbnail, - path: '/uploads/user-id/webp/path.ext', -}); - -const fullsizeFile = factory.assetFile({ - type: AssetFileType.FullSize, - path: '/uploads/user-id/fullsize/path.webp', -}); - -const files = [fullsizeFile, previewFile, thumbnailFile]; - -export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => { - return { - id: stackId, - assets, - ownerId: assets[0].ownerId, - primaryAsset: assets[0], - primaryAssetId: assets[0].id, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - updateId: expect.any(String), - }; -}; - -export const assetStub = { - storageAsset: (asset: Partial = {}) => ({ - id: 'asset-id', - ownerId: 'user-id', - livePhotoVideoId: null, - type: AssetType.Image, - isExternal: false, - checksum: Buffer.from('file hash'), - timeZone: null, - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - originalPath: '/original/path.jpg', - originalFileName: 'IMG_123.jpg', - fileSizeInByte: 12_345, - files: [], - make: 'FUJIFILM', - model: 'X-T50', - lensModel: 'XF27mm F2.8 R WR', - isEdited: false, - ...asset, - }), - noResizePath: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - originalFileName: 'IMG_123.jpg', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/library/IMG_123.jpg', - files: [thumbnailFile], - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - faces: [], - exifInfo: {} as Exif, - deletedAt: null, - isExternal: false, - duplicateId: null, - isOffline: false, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - primaryImage: Object.freeze({ - id: 'primary-asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.admin, - ownerId: 'admin-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - files, - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 1000, - exifImageWidth: 1000, - } as Exif, - stackId: 'stack-1', - stack: stackStub('stack-1', [ - { id: 'primary-asset-id' } as MapAsset & { exifInfo: Exif }, - { id: 'stack-child-asset-1' } as MapAsset & { exifInfo: Exif }, - { id: 'stack-child-asset-2' } as MapAsset & { exifInfo: Exif }, - ]), - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - image: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - files, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2025-01-01T01:02:03.456Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - updateId: 'foo', - libraryId: null, - stackId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - stack: null, - orientation: '', - projectionType: null, - height: null, - width: null, - visibility: AssetVisibility.Timeline, - edits: [], - isEdited: false, - }), - - trashed: Object.freeze({ - id: 'asset-id', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: new Date('2023-02-24T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: false, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - status: AssetStatus.Trashed, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - trashedOffline: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: new Date('2023-02-24T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: false, - duration: null, - libraryId: 'library-id', - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: true, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - archived: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - external: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/user1/photo.jpg', - checksum: Buffer.from('path hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - libraryId: 'library-id', - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - stackId: null, - stack: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - image1: Object.freeze({ - id: 'asset-id-1', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: null, - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - isExternal: false, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - stackId: null, - libraryId: null, - stack: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - video: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - originalFileName: 'asset-id.ext', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Video, - files: [previewFile], - thumbhash: null, - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - faces: [], - exifInfo: { - fileSizeInByte: 100_000, - exifImageHeight: 2160, - exifImageWidth: 3840, - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - livePhotoMotionAsset: Object.freeze({ - status: AssetStatus.Active, - id: fileStub.livePhotoMotion.uuid, - originalPath: fileStub.livePhotoMotion.originalPath, - ownerId: authStub.user1.user.id, - type: AssetType.Video, - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 100_000, - timeZone: `America/New_York`, - }, - files: [], - libraryId: null, - visibility: AssetVisibility.Hidden, - width: null, - height: null, - edits: [] as AssetEditActionItem[], - isEdited: false, - } as unknown as MapAsset & { - faces: AssetFace[]; - files: (AssetFile & { isProgressive: boolean })[]; - exifInfo: Exif; - edits: AssetEditActionItem[]; - }), - - livePhotoStillAsset: Object.freeze({ - id: 'live-photo-still-asset', - status: AssetStatus.Active, - originalPath: fileStub.livePhotoStill.originalPath, - ownerId: authStub.user1.user.id, - type: AssetType.Image, - livePhotoVideoId: 'live-photo-motion-asset', - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 25_000, - timeZone: `America/New_York`, - }, - files, - faces: [] as AssetFace[], - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [] as AssetEditActionItem[], - isEdited: false, - } as unknown as MapAsset & { - faces: AssetFace[]; - files: (AssetFile & { isProgressive: boolean })[]; - edits: AssetEditActionItem[]; - }), - - livePhotoWithOriginalFileName: Object.freeze({ - id: 'live-photo-still-asset', - status: AssetStatus.Active, - originalPath: fileStub.livePhotoStill.originalPath, - originalFileName: fileStub.livePhotoStill.originalName, - ownerId: authStub.user1.user.id, - type: AssetType.Image, - livePhotoVideoId: 'live-photo-motion-asset', - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 25_000, - timeZone: `America/New_York`, - }, - files: [] as AssetFile[], - libraryId: null, - faces: [] as AssetFace[], - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [] as AssetEditActionItem[], - isEdited: false, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }), - - withLocation: Object.freeze({ - id: 'asset-with-favorite-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - checksum: Buffer.from('file hash', 'utf8'), - originalPath: '/original/path.ext', - type: AssetType.Image, - files: [previewFile], - thumbhash: null, - encodedVideoPath: null, - createdAt: new Date('2023-02-22T05:06:29.716Z'), - updatedAt: new Date('2023-02-22T05:06:29.716Z'), - localDateTime: new Date('2020-12-31T23:59:00.000Z'), - isFavorite: false, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - updateId: 'foo', - libraryId: null, - stackId: null, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - exifInfo: { - latitude: 100, - longitude: 100, - fileSizeInByte: 23_456, - city: 'test-city', - state: 'test-state', - country: 'test-country', - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - tags: [], - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - hasEncodedVideo: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - originalFileName: 'asset-id.ext', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Video, - files: [previewFile], - thumbhash: null, - encodedVideoPath: '/encoded/video/path.mp4', - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - faces: [], - exifInfo: { - fileSizeInByte: 100_000, - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - stack: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - imageDng: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.dng', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.dng', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - profileDescription: 'Adobe RGB', - bitsPerSample: 14, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - width: null, - height: null, - edits: [], - isEdited: false, - }), - - withCropEdit: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - files, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2025-01-01T01:02:03.456Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - updateId: 'foo', - libraryId: null, - stackId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - sidecarPath: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - stack: null, - orientation: '', - projectionType: null, - height: 3840, - width: 2160, - visibility: AssetVisibility.Timeline, - edits: [ - { - action: AssetEditAction.Crop, - parameters: { - width: 1512, - height: 1152, - x: 216, - y: 1512, - }, - }, - ] as AssetEditActionItem[], - isEdited: true, - }), -}; diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts index 94a2dcff22..e01394e84f 100644 --- a/server/test/fixtures/face.stub.ts +++ b/server/test/fixtures/face.stub.ts @@ -1,13 +1,13 @@ import { SourceType } from 'src/enum'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { personStub } from 'test/fixtures/person.stub'; export const faceStub = { face1: Object.freeze({ id: 'assetFaceId1', - assetId: assetStub.image.id, + assetId: 'asset-id', asset: { - ...assetStub.image, + ...AssetFactory.create({ id: 'asset-id' }), libraryId: null, updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', stackId: null, @@ -29,8 +29,8 @@ export const faceStub = { }), primaryFace1: Object.freeze({ id: 'assetFaceId2', - assetId: assetStub.image.id, - asset: assetStub.image, + assetId: 'asset-id', + asset: AssetFactory.create({ id: 'asset-id' }), personId: personStub.primaryPerson.id, person: personStub.primaryPerson, boundingBoxX1: 0, @@ -48,8 +48,8 @@ export const faceStub = { }), mergeFace1: Object.freeze({ id: 'assetFaceId3', - assetId: assetStub.image.id, - asset: assetStub.image, + assetId: 'asset-id', + asset: AssetFactory.create({ id: 'asset-id' }), personId: personStub.mergePerson.id, person: personStub.mergePerson, boundingBoxX1: 0, @@ -67,8 +67,8 @@ export const faceStub = { }), noPerson1: Object.freeze({ id: 'assetFaceId8', - assetId: assetStub.image.id, - asset: assetStub.image, + assetId: 'asset-id', + asset: AssetFactory.create({ id: 'asset-id' }), personId: null, person: null, boundingBoxX1: 0, @@ -86,8 +86,8 @@ export const faceStub = { }), noPerson2: Object.freeze({ id: 'assetFaceId9', - assetId: assetStub.image.id, - asset: assetStub.image, + assetId: 'asset-id', + asset: AssetFactory.create({ id: 'asset-id' }), personId: null, person: null, boundingBoxX1: 0, @@ -105,8 +105,8 @@ export const faceStub = { }), fromExif1: Object.freeze({ id: 'assetFaceId9', - assetId: assetStub.image.id, - asset: assetStub.image, + assetId: 'asset-id', + asset: AssetFactory.create({ id: 'asset-id' }), personId: personStub.randomPerson.id, person: personStub.randomPerson, boundingBoxX1: 100, @@ -123,8 +123,8 @@ export const faceStub = { }), fromExif2: Object.freeze({ id: 'assetFaceId9', - assetId: assetStub.image.id, - asset: assetStub.image, + assetId: 'asset-id', + asset: AssetFactory.create({ id: 'asset-id' }), personId: personStub.randomPerson.id, person: personStub.randomPerson, boundingBoxX1: 0, @@ -141,8 +141,8 @@ export const faceStub = { }), withBirthDate: Object.freeze({ id: 'assetFaceId10', - assetId: assetStub.image.id, - asset: assetStub.image, + assetId: 'asset-id', + asset: AssetFactory.create({ id: 'asset-id' }), personId: personStub.withBirthDate.id, person: personStub.withBirthDate, boundingBoxX1: 0, diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index 35a7a8ed7d..9d48fcc8f8 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -1,5 +1,5 @@ -import { AssetType } from 'src/enum'; -import { previewFile } from 'test/fixtures/asset.stub'; +import { AssetFileType, AssetType } from 'src/enum'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; import { userStub } from 'test/fixtures/user.stub'; const updateId = '0d1173e3-4d80-4d76-b41e-57d56de21125'; @@ -179,7 +179,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), newThumbnailMiddle: Object.freeze({ ownerId: userStub.admin.id, @@ -192,7 +192,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), newThumbnailEnd: Object.freeze({ ownerId: userStub.admin.id, @@ -205,7 +205,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), rawEmbeddedThumbnail: Object.freeze({ ownerId: userStub.admin.id, @@ -218,7 +218,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.dng', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), negativeCoordinate: Object.freeze({ ownerId: userStub.admin.id, @@ -231,7 +231,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), overflowingCoordinate: Object.freeze({ ownerId: userStub.admin.id, @@ -244,7 +244,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), videoThumbnail: Object.freeze({ ownerId: userStub.admin.id, @@ -257,6 +257,6 @@ export const personThumbnailStub = { type: AssetType.Video, originalPath: '/original/path.mp4', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 859b6b6ae2..a42ff743bc 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -2,7 +2,7 @@ import { UserAdmin } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; import { AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -31,7 +31,7 @@ export const sharedLinkStub = { albumId: null, album: null, description: null, - assets: [assetStub.image], + assets: [AssetFactory.create()], password: 'password', slug: null, }), diff --git a/server/test/mappers.ts b/server/test/mappers.ts new file mode 100644 index 0000000000..89ca79d864 --- /dev/null +++ b/server/test/mappers.ts @@ -0,0 +1,22 @@ +import { AssetFactory } from 'test/factories/asset.factory'; + +export const getForStorageTemplate = (asset: ReturnType) => { + return { + id: asset.id, + ownerId: asset.ownerId, + livePhotoVideoId: asset.livePhotoVideoId, + type: asset.type, + isExternal: asset.isExternal, + checksum: asset.checksum, + timeZone: asset.exifInfo.timeZone, + fileCreatedAt: asset.fileCreatedAt, + originalPath: asset.originalPath, + originalFileName: asset.originalFileName, + fileSizeInByte: asset.exifInfo.fileSizeInByte, + files: asset.files, + make: asset.exifInfo.make, + model: asset.exifInfo.model, + lensModel: asset.exifInfo.lensModel, + isEdited: asset.isEdited, + }; +}; diff --git a/server/test/medium/specs/repositories/asset-job.repository.spec.ts b/server/test/medium/specs/repositories/asset-job.repository.spec.ts new file mode 100644 index 0000000000..6af3aa778f --- /dev/null +++ b/server/test/medium/specs/repositories/asset-job.repository.spec.ts @@ -0,0 +1,118 @@ +import { Kysely } from 'kysely'; +import { AssetFileType } from 'src/enum'; +import { AssetJobRepository } from 'src/repositories/asset-job.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +const consume = async (generator: AsyncIterableIterator) => { + const values: T[] = []; + + for await (const value of generator) { + values.push(value); + } + + return values; +}; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(AssetJobRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AssetJobRepository.name, () => { + describe('streamForThumbnailJob', () => { + it('should work', async () => { + const { sut } = setup(); + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: false }); + await expect(stream.next()).resolves.toEqual({ done: true, value: undefined }); + }); + + it('should queue an asset with missing thumbnails', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: false }); + await expect(consume(stream)).resolves.toEqual([expect.objectContaining({ id: asset.id })]); + }); + + it('should skip assets without missing thumbnails', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, thumbhash: Buffer.from('fake-thumbhash-buffer') }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: false }); + await expect(consume(stream)).resolves.not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + + it('should queue assets with a missing full size', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ + ownerId: user.id, + thumbhash: Buffer.from('fake-thumbhash-buffer'), + originalFileName: 'photo.cr2', + }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: true }); + await expect(consume(stream)).resolves.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + + it('should skip assets with after they have full size previews', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, thumbhash: Buffer.from('fake-thumbhash-buffer') }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.FullSize, path: 'fullsize.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: true }); + await expect(consume(stream)).resolves.not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + + it('should skip assets with web-compatible originals', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ + ownerId: user.id, + thumbhash: Buffer.from('fake-thumbhash-buffer'), + originalFileName: 'photo.jpg', + }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: true }); + await expect(consume(stream)).resolves.not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + }); +}); diff --git a/server/test/medium/specs/repositories/person.repository.spec.ts b/server/test/medium/specs/repositories/person.repository.spec.ts new file mode 100644 index 0000000000..30f0fd33c0 --- /dev/null +++ b/server/test/medium/specs/repositories/person.repository.spec.ts @@ -0,0 +1,68 @@ +import { Kysely } from 'kysely'; +import { AssetFileType } from 'src/enum'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(PersonRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(PersonRepository.name, () => { + describe('getDataForThumbnailGenerationJob', () => { + it('should not return the edited preview path', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const { person } = await ctx.newPerson({ ownerId: user.id }); + + const { assetFace } = await ctx.newAssetFace({ + assetId: asset.id, + personId: person.id, + boundingBoxX1: 10, + boundingBoxY1: 10, + boundingBoxX2: 90, + boundingBoxY2: 90, + }); + + // theres a circular dependency between assetFace and person, so we need to update the person after creating the assetFace + await ctx.database.updateTable('person').set({ faceAssetId: assetFace.id }).where('id', '=', person.id).execute(); + + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: 'preview_edited.jpg', + isEdited: true, + }); + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: 'preview_unedited.jpg', + isEdited: false, + }); + + const result = await sut.getDataForThumbnailGenerationJob(person.id); + + expect(result).toEqual( + expect.objectContaining({ + previewPath: 'preview_unedited.jpg', + }), + ); + }); + }); +}); diff --git a/server/test/medium/specs/services/person.service.spec.ts b/server/test/medium/specs/services/person.service.spec.ts index a13f64032c..c86bd96a10 100644 --- a/server/test/medium/specs/services/person.service.spec.ts +++ b/server/test/medium/specs/services/person.service.spec.ts @@ -685,5 +685,75 @@ describe(PersonService.name, () => { ]), ); }); + + it('should properly handle exif orientation when creating a face on an edited asset', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 100 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 100, orientation: '6' }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Vertical, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 100, + imageHeight: 100, + x: 10, + y: 10, + width: 80, + height: 80, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 110, + boundingBoxY1: 10, + boundingBoxX2: 190, + boundingBoxY2: 90, + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 10, + boundingBoxY1: 10, + boundingBoxX2: 90, + boundingBoxY2: 90, + }), + ]), + ); + }); }); }); diff --git a/server/test/medium/specs/services/shared-link.service.spec.ts b/server/test/medium/specs/services/shared-link.service.spec.ts index acc51374d1..a43d0de9b9 100644 --- a/server/test/medium/specs/services/shared-link.service.spec.ts +++ b/server/test/medium/specs/services/shared-link.service.spec.ts @@ -90,7 +90,7 @@ describe(SharedLinkService.name, () => { assetIds: assets.map(({ asset }) => asset.id), }); - await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({ + await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({ assets: assets.map(({ asset }) => expect.objectContaining({ id: asset.id })), }); }); @@ -114,7 +114,7 @@ describe(SharedLinkService.name, () => { assetIds: [asset.id], }); - await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({ + await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({ assets: [expect.objectContaining({ id: asset.id })], }); @@ -122,6 +122,6 @@ describe(SharedLinkService.name, () => { assetIds: [asset.id], }); - await expect(sut.getMine({ user, sharedLink }, {})).resolves.toHaveProperty('assets', []); + await expect(sut.getMine({ user, sharedLink }, [])).resolves.toHaveProperty('assets', []); }); }); diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts deleted file mode 100644 index 0ff869ca28..0000000000 --- a/server/test/repositories/database.repository.mock.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { DatabaseRepository } from 'src/repositories/database.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newDatabaseRepositoryMock = (): Mocked> => { - return { - shutdown: vitest.fn(), - getExtensionVersions: vitest.fn(), - getVectorExtension: vitest.fn(), - getExtensionVersionRange: vitest.fn(), - getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'), - getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'), - createExtension: vitest.fn().mockResolvedValue(void 0), - dropExtension: vitest.fn(), - updateVectorExtension: vitest.fn(), - reindexVectorsIfNeeded: vitest.fn(), - getDimensionSize: vitest.fn(), - setDimensionSize: vitest.fn(), - deleteAllSearchEmbeddings: vitest.fn(), - prewarm: vitest.fn(), - runMigrations: vitest.fn(), - revertLastMigration: vitest.fn(), - withLock: vitest.fn().mockImplementation((_, function_: () => Promise) => function_()), - tryLock: vitest.fn(), - isBusy: vitest.fn(), - wait: vitest.fn(), - migrateFilePaths: vitest.fn(), - }; -}; diff --git a/server/test/utils.ts b/server/test/utils.ts index cd866994eb..c2a83c52ae 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -75,7 +75,6 @@ import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositorie import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; @@ -279,6 +278,14 @@ export const getMocks = () => { const loggerMock = { setContext: () => {} }; const configMock = { getEnv: () => ({}) }; + // eslint-disable-next-line no-sparse-arrays + const databaseMock = automock(DatabaseRepository, { args: [, loggerMock], strict: false }); + + databaseMock.withLock.mockImplementation((_type, fn) => fn()); + databaseMock.getPostgresVersion = vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'); + databaseMock.getPostgresVersionRange = vitest.fn().mockReturnValue('>=14.0.0'); + databaseMock.createExtension = vitest.fn().mockResolvedValue(void 0); + const mocks: ServiceMocks = { access: newAccessRepositoryMock(), // eslint-disable-next-line no-sparse-arrays @@ -295,7 +302,7 @@ export const getMocks = () => { assetJob: automock(AssetJobRepository), app: automock(AppRepository, { strict: false }), config: newConfigRepositoryMock(), - database: newDatabaseRepositoryMock(), + database: databaseMock, downloadRepository: automock(DownloadRepository, { strict: false }), duplicateRepository: automock(DuplicateRepository), email: automock(EmailRepository, { args: [loggerMock] }), @@ -496,10 +503,12 @@ export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: st } as unknown as ChildProcessWithoutNullStreams; }); -export const mockDuplex = vitest.fn( +export const mockDuplex = + (chunkCb?: (chunk: Buffer) => void) => (command: string, exitCode: number, stdout: string, stderr: string, error?: unknown) => { const duplex = new Duplex({ - write(_chunk, _encoding, callback) { + write(chunk, _encoding, callback) { + chunkCb?.(chunk); callback(); }, @@ -524,8 +533,7 @@ export const mockDuplex = vitest.fn( }); return duplex; - }, -); + }; export const mockFork = vitest.fn((exitCode: number, stdout: string, stderr: string, error?: unknown) => { const stdoutStream = new Readable({ diff --git a/web/package.json b/web/package.json index b8a5f1d494..60333b101e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "2.5.5", + "version": "2.5.6", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { @@ -26,8 +26,8 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", - "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.61.4", + "@immich/sdk": "workspace:*", + "@immich/ui": "^0.62.1", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", @@ -37,7 +37,7 @@ "@photo-sphere-viewer/settings-plugin": "^5.14.0", "@photo-sphere-viewer/video-plugin": "^5.14.0", "@types/geojson": "^7946.0.16", - "@zoom-image/core": "^0.41.0", + "@zoom-image/core": "^0.42.0", "@zoom-image/svelte": "^0.3.0", "dom-to-image": "^2.6.0", "fabric": "^6.5.4", @@ -71,7 +71,7 @@ "@koddsson/eslint-plugin-tscompat": "^0.2.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/enhanced-img": "^0.9.0", + "@sveltejs/enhanced-img": "^0.10.0", "@sveltejs/kit": "^2.27.1", "@sveltejs/vite-plugin-svelte": "6.2.4", "@tailwindcss/vite": "^4.1.7", @@ -99,7 +99,7 @@ "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", "rollup-plugin-visualizer": "^6.0.0", - "svelte": "5.48.0", + "svelte": "5.50.0", "svelte-check": "^4.1.5", "svelte-eslint-parser": "^1.3.3", "tailwindcss": "^4.1.7", diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts index 8f01ce8924..b047dfc391 100644 --- a/web/src/lib/actions/shortcut.ts +++ b/web/src/lib/actions/shortcut.ts @@ -1,112 +1,9 @@ -import type { ActionReturn } from 'svelte/action'; - -export type Shortcut = { - key: string; - alt?: boolean; - ctrl?: boolean; - shift?: boolean; - meta?: boolean; -}; - -export type ShortcutOptions = { - shortcut: Shortcut; - /** If true, the event handler will not execute if the event comes from an input field */ - ignoreInputFields?: boolean; - onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown; - preventDefault?: boolean; -}; - -export const shortcutLabel = (shortcut: Shortcut) => { - let label = ''; - - if (shortcut.ctrl) { - label += 'Ctrl '; - } - if (shortcut.alt) { - label += 'Alt '; - } - if (shortcut.meta) { - label += 'Cmd '; - } - if (shortcut.shift) { - label += '⇧'; - } - label += shortcut.key.toUpperCase(); - - return label; -}; - -/** Determines whether an event should be ignored. The event will be ignored if: - * - The element dispatching the event is not the same as the element which the event listener is attached to - * - The element dispatching the event is an input field - * - The element dispatching the event is a map canvas - */ -export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => { - if (event.target === event.currentTarget) { - return false; - } - const type = (event.target as HTMLInputElement).type; - return ( - ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type) || - (event.target instanceof HTMLCanvasElement && event.target.classList.contains('maplibregl-canvas')) - ); -}; - -export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => { - return ( - shortcut.key.toLowerCase() === event.key.toLowerCase() && - Boolean(shortcut.alt) === event.altKey && - Boolean(shortcut.ctrl) === event.ctrlKey && - Boolean(shortcut.shift) === event.shiftKey && - Boolean(shortcut.meta) === event.metaKey - ); -}; - -/** Bind a single keyboard shortcut to node. */ -export const shortcut = ( - node: T, - option: ShortcutOptions, -): ActionReturn> => { - const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]); - - return { - update(newOption) { - shortcutsUpdate?.([newOption]); - }, - destroy, - }; -}; - -/** Binds multiple keyboard shortcuts to node */ -export const shortcuts = ( - node: T, - options: ShortcutOptions[], -): ActionReturn[]> => { - function onKeydown(event: KeyboardEvent) { - const ignoreShortcut = shouldIgnoreEvent(event); - for (const { shortcut, onShortcut, ignoreInputFields = true, preventDefault = true } of options) { - if (ignoreInputFields && ignoreShortcut) { - continue; - } - - if (matchesShortcut(event, shortcut)) { - if (preventDefault) { - event.preventDefault(); - } - onShortcut(event as KeyboardEvent & { currentTarget: T }); - return; - } - } - } - - node.addEventListener('keydown', onKeydown); - - return { - update(newOptions) { - options = newOptions; - }, - destroy() { - node.removeEventListener('keydown', onKeydown); - }, - }; -}; +export { + matchesShortcut, + shortcut, + shortcutLabel, + shortcuts, + shouldIgnoreEvent, + type Shortcut, + type ShortcutOptions, +} from '@immich/ui'; diff --git a/web/src/lib/components/SharedLinkExpiration.svelte b/web/src/lib/components/SharedLinkExpiration.svelte index f8f6167084..bfe1241482 100644 --- a/web/src/lib/components/SharedLinkExpiration.svelte +++ b/web/src/lib/components/SharedLinkExpiration.svelte @@ -1,21 +1,16 @@
- + + + + +
+ {#each expiredDateOptions as option (option.value)} + + {/each} +
diff --git a/web/src/lib/components/SharedLinkFormFields.spec.ts b/web/src/lib/components/SharedLinkFormFields.spec.ts index 9c65c43833..18efa46c65 100644 --- a/web/src/lib/components/SharedLinkFormFields.spec.ts +++ b/web/src/lib/components/SharedLinkFormFields.spec.ts @@ -1,4 +1,4 @@ -import { render } from '@testing-library/svelte'; +import { renderWithTooltips } from '$tests/helpers'; import userEvent from '@testing-library/user-event'; import SharedLinkFormFields from './SharedLinkFormFields.svelte'; @@ -7,16 +7,14 @@ describe('SharedLinkFormFields component', () => { element instanceof HTMLInputElement ? element.checked : element.getAttribute('aria-checked') === 'true'; it('turns downloads off when metadata is disabled', async () => { - const { container } = render(SharedLinkFormFields, { - props: { - slug: '', - password: '', - description: '', - allowDownload: true, - allowUpload: false, - showMetadata: true, - expiresAt: null, - }, + const { container } = renderWithTooltips(SharedLinkFormFields, { + slug: '', + password: '', + description: '', + allowDownload: true, + allowUpload: false, + showMetadata: true, + expiresAt: null, }); const user = userEvent.setup(); diff --git a/web/src/lib/components/SharedLinkFormFields.svelte b/web/src/lib/components/SharedLinkFormFields.svelte index 1e7b3b754b..42019fe54e 100644 --- a/web/src/lib/components/SharedLinkFormFields.svelte +++ b/web/src/lib/components/SharedLinkFormFields.svelte @@ -11,7 +11,6 @@ allowUpload: boolean; showMetadata: boolean; expiresAt: string | null; - createdAt?: string; }; let { @@ -22,7 +21,6 @@ allowUpload = $bindable(), showMetadata = $bindable(), expiresAt = $bindable(), - createdAt, }: Props = $props(); $effect(() => { @@ -50,7 +48,7 @@ - + diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 24fa8c83a2..e3ac0ed61f 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -68,7 +68,8 @@ let currentMemoryAssetFull = $derived.by(async () => current?.asset ? await getAssetInfo({ ...authManager.params, id: current.asset.id }) : undefined, ); - let currentTimelineAssets = $derived([ + let currentTimelineAssets = $derived(current?.memory.assets ?? []); + let viewerAssets = $derived([ ...(current?.previousMemory?.assets ?? []), ...(current?.memory.assets ?? []), ...(current?.nextMemory?.assets ?? []), @@ -651,6 +652,7 @@ > { try { - sharedLink = await getMySharedLink({ password, key, slug }); + sharedLink = await sharedLinkLogin({ key, slug, sharedLinkLoginDto: { password } }); setSharedLink(sharedLink); passwordRequired = false; title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich'; diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index ef4e26e849..7c844cb4d9 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -1,7 +1,5 @@ diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte index ee28e66bff..9bbffe53ef 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -1,5 +1,5 @@
- {#if $isPurchased && $preferences.purchase.showSupportBadge} + {#if authManager.isPurchased && $preferences.purchase.showSupportBadge} - {:else if !$isPurchased && showBuyButton && getAccountAge() > 14} + {:else if !authManager.isPurchased && showBuyButton && getAccountAge() > 14}
-
+
{#each subItems as item (item)}
{ - if (!$isPurchased) { + if (!authManager.isPurchased) { return; } @@ -73,7 +72,7 @@ } await deleteIndividualProductKey(); - purchaseStore.setPurchaseStatus(false); + authManager.isPurchased = false; } catch (error) { handleError(error, $t('errors.failed_to_remove_product_key')); } @@ -92,21 +91,21 @@ } await deleteServerProductKey(); - purchaseStore.setPurchaseStatus(false); + authManager.isPurchased = false; } catch (error) { handleError(error, $t('errors.failed_to_remove_product_key')); } }; const onProductActivated = async () => { - purchaseStore.setPurchaseStatus(true); + authManager.isPurchased = true; await checkPurchaseInfo(); };
- {#if $isPurchased} + {#if authManager.isPurchased}
import('$i18n/zh_SIMPLIFIED.json'), + weblateCode: 'zh_Hans', + loader: () => import('$i18n/zh_Hans.json'), }, { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({ default: {} }) }, ]; diff --git a/web/src/lib/managers/auth-manager.svelte.ts b/web/src/lib/managers/auth-manager.svelte.ts index e96f3c1449..317894b0ad 100644 --- a/web/src/lib/managers/auth-manager.svelte.ts +++ b/web/src/lib/managers/auth-manager.svelte.ts @@ -3,12 +3,31 @@ import { page } from '$app/state'; import { eventManager } from '$lib/managers/event-manager.svelte'; import { Route } from '$lib/route'; import { isSharedLinkRoute } from '$lib/utils/navigation'; -import { logout } from '@immich/sdk'; +import { getAboutInfo, logout, type UserAdminResponseDto } from '@immich/sdk'; class AuthManager { + isPurchased = $state(false); isSharedLink = $derived(isSharedLinkRoute(page.route?.id)); params = $derived(this.isSharedLink ? { key: page.params.key, slug: page.params.slug } : {}); + constructor() { + eventManager.on({ + AuthUserLoaded: (user) => this.onAuthUserLoaded(user), + }); + } + + private async onAuthUserLoaded(user: UserAdminResponseDto) { + if (user.license?.activatedAt) { + authManager.isPurchased = true; + return; + } + + const serverInfo = await getAboutInfo().catch(() => undefined); + if (serverInfo?.licensed) { + authManager.isPurchased = true; + } + } + async logout() { let redirectUri; @@ -30,6 +49,7 @@ class AuthManager { globalThis.location.href = redirectUri; } } finally { + this.isPurchased = false; eventManager.emit('AuthLogout'); } } diff --git a/web/src/lib/managers/timeline-manager/day-group.svelte.ts b/web/src/lib/managers/timeline-manager/day-group.svelte.ts index e21e54a6e5..ba12f4bb6c 100644 --- a/web/src/lib/managers/timeline-manager/day-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/day-group.svelte.ts @@ -102,25 +102,21 @@ export class DayGroup { } runAssetCallback(ids: Set, callback: (asset: TimelineAsset) => void | { remove?: boolean }) { - if (ids.size === 0) { - return { - moveAssets: [] as MoveAsset[], - processedIds: new SvelteSet(), - unprocessedIds: ids, - changedGeometry: false, - }; - } const unprocessedIds = new SvelteSet(ids); const processedIds = new SvelteSet(); const moveAssets: MoveAsset[] = []; let changedGeometry = false; - for (const assetId of unprocessedIds) { - const index = this.viewerAssets.findIndex((viewAsset) => viewAsset.id == assetId); - if (index === -1) { + + if (ids.size === 0) { + return { moveAssets, processedIds, unprocessedIds, changedGeometry }; + } + + for (let index = this.viewerAssets.length - 1; index >= 0; index--) { + const { id: assetId, asset } = this.viewerAssets[index]; + if (!ids.has(assetId)) { continue; } - const asset = this.viewerAssets[index].asset!; const oldTime = { ...asset.localDateTime }; const callbackResult = callback(asset); let remove = (callbackResult as { remove?: boolean } | undefined)?.remove ?? false; diff --git a/web/src/lib/modals/AssetTagModal.svelte b/web/src/lib/modals/AssetTagModal.svelte index c0c7f8b10a..74daf75659 100644 --- a/web/src/lib/modals/AssetTagModal.svelte +++ b/web/src/lib/modals/AssetTagModal.svelte @@ -10,7 +10,7 @@ import Combobox, { type ComboBoxOption } from '../components/shared-components/combobox.svelte'; interface Props { - onClose: () => void; + onClose: (updated?: boolean) => void; assetIds: string[]; } @@ -33,7 +33,7 @@ const updatedIds = await tagAssets({ tagIds: [...selectedIds], assetIds, showNotification: false }); eventManager.emit('AssetsTag', updatedIds); - onClose(); + onClose(true); }; const handleSelect = async (option?: ComboBoxOption) => { diff --git a/web/src/lib/services/asset.service.ts b/web/src/lib/services/asset.service.ts index 0c1c96ab04..f9b33d5687 100644 --- a/web/src/lib/services/asset.service.ts +++ b/web/src/lib/services/asset.service.ts @@ -5,7 +5,8 @@ import { eventManager } from '$lib/managers/event-manager.svelte'; import AssetTagModal from '$lib/modals/AssetTagModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import { user as authUser, preferences } from '$lib/stores/user.store'; -import { getAssetJobName, getSharedLink, sleep } from '$lib/utils'; +import type { AssetControlContext } from '$lib/types'; +import { getSharedLink, sleep } from '$lib/utils'; import { downloadUrl } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; @@ -48,6 +49,44 @@ import { import type { MessageFormatter } from 'svelte-i18n'; import { get } from 'svelte/store'; +export const getAssetBulkActions = ($t: MessageFormatter, ctx: AssetControlContext) => { + const ownedAssets = ctx.getOwnedAssets(); + const assetIds = ownedAssets.map((asset) => asset.id); + const isAllVideos = ownedAssets.every((asset) => asset.isVideo); + + const onAction = async (name: AssetJobName) => { + await handleRunAssetJob({ name, assetIds }); + ctx.clearSelect(); + }; + + const RefreshFacesJob: ActionItem = { + title: $t('refresh_faces'), + icon: mdiHeadSyncOutline, + onAction: () => onAction(AssetJobName.RefreshFaces), + }; + + const RefreshMetadataJob: ActionItem = { + title: $t('refresh_metadata'), + icon: mdiDatabaseRefreshOutline, + onAction: () => onAction(AssetJobName.RefreshMetadata), + }; + + const RegenerateThumbnailJob: ActionItem = { + title: $t('refresh_thumbnails'), + icon: mdiImageRefreshOutline, + onAction: () => onAction(AssetJobName.RegenerateThumbnail), + }; + + const TranscodeVideoJob: ActionItem = { + title: $t('refresh_encoded_videos'), + icon: mdiCogRefreshOutline, + onAction: () => onAction(AssetJobName.TranscodeVideo), + $if: () => isAllVideos, + }; + + return { RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob }; +}; + export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => { const sharedLink = getSharedLink(); const currentAuthUser = get(authUser); @@ -186,25 +225,25 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) = }; const RefreshFacesJob: ActionItem = { - title: getAssetJobName($t, AssetJobName.RefreshFaces), + title: $t('refresh_faces'), icon: mdiHeadSyncOutline, onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshFaces, assetIds: [asset.id] }), }; const RefreshMetadataJob: ActionItem = { - title: getAssetJobName($t, AssetJobName.RefreshMetadata), + title: $t('refresh_metadata'), icon: mdiDatabaseRefreshOutline, onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshMetadata, assetIds: [asset.id] }), }; const RegenerateThumbnailJob: ActionItem = { - title: getAssetJobName($t, AssetJobName.RegenerateThumbnail), + title: $t('refresh_thumbnails'), icon: mdiImageRefreshOutline, onAction: () => handleRunAssetJob({ name: AssetJobName.RegenerateThumbnail, assetIds: [asset.id] }), }; const TranscodeVideoJob: ActionItem = { - title: getAssetJobName($t, AssetJobName.TranscodeVideo), + title: $t('refresh_encoded_videos'), icon: mdiCogRefreshOutline, onAction: () => handleRunAssetJob({ name: AssetJobName.TranscodeVideo, assetIds: [asset.id] }), $if: () => asset.type === AssetTypeEnum.Video, @@ -313,12 +352,23 @@ export const handleReplaceAsset = async (oldAssetId: string) => { eventManager.emit('AssetReplace', { oldAssetId, newAssetId }); }; +const getAssetJobMessage = ($t: MessageFormatter, job: AssetJobName) => { + const messages: Record = { + [AssetJobName.RefreshFaces]: $t('refreshing_faces'), + [AssetJobName.RefreshMetadata]: $t('refreshing_metadata'), + [AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'), + [AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'), + }; + + return messages[job]; +}; + const handleRunAssetJob = async (dto: AssetJobsDto) => { const $t = await getFormatter(); try { await runAssetJobs({ assetJobsDto: dto }); - toastManager.success(getAssetJobName($t, dto.name)); + toastManager.success(getAssetJobMessage($t, dto.name)); } catch (error) { handleError(error, $t('errors.unable_to_submit_job')); } diff --git a/web/src/lib/stores/asset-interaction.svelte.ts b/web/src/lib/stores/asset-interaction.svelte.ts index 9dee11f238..817354e619 100644 --- a/web/src/lib/stores/asset-interaction.svelte.ts +++ b/web/src/lib/stores/asset-interaction.svelte.ts @@ -1,13 +1,15 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { user } from '$lib/stores/user.store'; import { AssetVisibility, type UserAdminResponseDto } from '@immich/sdk'; -import { SvelteSet } from 'svelte/reactivity'; +import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { fromStore } from 'svelte/store'; export class AssetInteraction { - selectedAssets = $state([]); + private selectedAssetsMap = new SvelteMap(); + selectedAssets = $derived(Array.from(this.selectedAssetsMap.values())); + selectAll = $state(false); hasSelectedAsset(assetId: string) { - return this.selectedAssets.some((asset) => asset.id === assetId); + return this.selectedAssetsMap.has(assetId); } selectedGroup = new SvelteSet(); assetSelectionCandidates = $state([]); @@ -15,7 +17,7 @@ export class AssetInteraction { return this.assetSelectionCandidates.some((asset) => asset.id === assetId); } assetSelectionStart = $state(null); - selectionActive = $derived(this.selectedAssets.length > 0); + selectionActive = $derived(this.selectedAssetsMap.size > 0); private user = fromStore(user); private userId = $derived(this.user.current?.id); @@ -26,9 +28,7 @@ export class AssetInteraction { isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId)); selectAsset(asset: TimelineAsset) { - if (!this.hasSelectedAsset(asset.id)) { - this.selectedAssets.push(asset); - } + this.selectedAssetsMap.set(asset.id, asset); } selectAssets(assets: TimelineAsset[]) { @@ -38,10 +38,7 @@ export class AssetInteraction { } removeAssetFromMultiselectGroup(assetId: string) { - const index = this.selectedAssets.findIndex((a) => a.id == assetId); - if (index !== -1) { - this.selectedAssets.splice(index, 1); - } + this.selectedAssetsMap.delete(assetId); } addGroupToMultiselectGroup(group: string) { @@ -65,8 +62,10 @@ export class AssetInteraction { } clearMultiselect() { + this.selectAll = false; + // Multi-selection - this.selectedAssets = []; + this.selectedAssetsMap.clear(); this.selectedGroup.clear(); // Range selection diff --git a/web/src/lib/stores/assets-store.svelte.ts b/web/src/lib/stores/assets-store.svelte.ts deleted file mode 100644 index 4607f402b4..0000000000 --- a/web/src/lib/stores/assets-store.svelte.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { writable } from 'svelte/store'; - -export const isSelectingAllAssets = writable(false); diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index cf66bb488f..f3cff35773 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -9,7 +9,7 @@ export interface ThemeSetting { } // Locale to use for formatting dates, numbers, etc. -export const locale = persisted('locale', 'default', { +export const locale = persisted('locale', 'default', { serializer: { parse: (text) => text || 'default', stringify: (object) => object ?? '', diff --git a/web/src/lib/stores/purchase.store.ts b/web/src/lib/stores/purchase.store.ts deleted file mode 100644 index 4b9c61eed7..0000000000 --- a/web/src/lib/stores/purchase.store.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { readonly, writable } from 'svelte/store'; - -function createPurchaseStore() { - const isPurcharsed = writable(false); - - function setPurchaseStatus(status: boolean) { - isPurcharsed.set(status); - } - - return { - isPurchased: readonly(isPurcharsed), - setPurchaseStatus, - }; -} - -export const purchaseStore = createPurchaseStore(); diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts index 331df7ad5f..3c1549c2cd 100644 --- a/web/src/lib/stores/user.store.ts +++ b/web/src/lib/stores/user.store.ts @@ -1,5 +1,4 @@ import { eventManager } from '$lib/managers/event-manager.svelte'; -import { purchaseStore } from '$lib/stores/purchase.store'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { writable } from 'svelte/store'; @@ -13,7 +12,6 @@ export const preferences = writable(); export const resetSavedUser = () => { user.set(undefined as unknown as UserAdminResponseDto); preferences.set(undefined as unknown as UserPreferencesResponseDto); - purchaseStore.setPurchaseStatus(false); }; eventManager.on({ diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 134ac17b60..75d3a3ac40 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -1,3 +1,4 @@ +import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk'; import type { ActionItem } from '@immich/ui'; @@ -40,3 +41,10 @@ export enum OnboardingRole { SERVER = 'server', USER = 'user', } + +export type AssetControlContext = { + // Wrap assets in a function, because context isn't reactive. + getAssets: () => TimelineAsset[]; // All assets includes partners' assets + getOwnedAssets: () => TimelineAsset[]; // Only assets owned by the user + clearSelect: () => void; +}; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 0163ee819c..3204b35576 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -4,7 +4,6 @@ import { alwaysLoadOriginalFile, lang } from '$lib/stores/preferences.store'; import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { - AssetJobName, AssetMediaSize, AssetTypeEnum, MemoryType, @@ -27,8 +26,7 @@ import { type UserResponseDto, } from '@immich/sdk'; import { toastManager, type ActionItem, type IfLike } from '@immich/ui'; -import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js'; -import { init, register, t, type MessageFormatter } from 'svelte-i18n'; +import { init, register, t } from 'svelte-i18n'; import { derived, get } from 'svelte/store'; interface DownloadRequestOptions { @@ -245,28 +243,6 @@ export const getProfileImageUrl = (user: UserResponseDto) => export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) => createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt }); -export const getAssetJobName = ($t: MessageFormatter, job: AssetJobName) => { - const messages: Record = { - [AssetJobName.RefreshFaces]: $t('refreshing_faces'), - [AssetJobName.RefreshMetadata]: $t('refreshing_metadata'), - [AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'), - [AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'), - }; - - return messages[job]; -}; - -export const getAssetJobIcon = (job: AssetJobName) => { - const names: Record = { - [AssetJobName.RefreshFaces]: mdiHeadSyncOutline, - [AssetJobName.RefreshMetadata]: mdiDatabaseRefreshOutline, - [AssetJobName.RegenerateThumbnail]: mdiImageRefreshOutline, - [AssetJobName.TranscodeVideo]: mdiCogRefreshOutline, - }; - - return names[job]; -}; - export const copyToClipboard = async (secret: string) => { const $t = get(t); diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 18d2460210..47df967844 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -4,10 +4,8 @@ import { authManager } from '$lib/managers/auth-manager.svelte'; import { downloadManager } from '$lib/managers/download-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; -import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte'; import { Route } from '$lib/route'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; -import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { preferences } from '$lib/stores/user.store'; import { downloadRequest, withError } from '$lib/utils'; import { getByteUnitString } from '$lib/utils/byte-units'; @@ -304,9 +302,18 @@ const supportedImageMimeTypes = new Set([ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755 if (isSafari) { - supportedImageMimeTypes.add('image/heic').add('image/heif').add('image/jxl'); + supportedImageMimeTypes.add('image/heic').add('image/heif'); } +function checkJxlSupport(): void { + const img = new Image(); + img.addEventListener('load', () => { + supportedImageMimeTypes.add('image/jxl'); + }); + img.src = 'data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA='; // Small valid JPEG XL image +} +checkJxlSupport(); + /** * Returns true if the asset is an image supported by web browsers, false otherwise */ @@ -427,21 +434,23 @@ export const keepThisDeleteOthers = async (keepAsset: AssetResponseDto, stack: S }; export const selectAllAssets = async (timelineManager: TimelineManager, assetInteraction: AssetInteraction) => { - if (get(isSelectingAllAssets)) { + if (assetInteraction.selectAll) { // Selection is already ongoing return; } - isSelectingAllAssets.set(true); + assetInteraction.selectAll = true; try { for (const monthGroup of timelineManager.months) { - await timelineManager.loadMonthGroup(monthGroup.yearMonth); + if (!monthGroup.isLoaded) { + await timelineManager.loadMonthGroup(monthGroup.yearMonth); + } - if (!get(isSelectingAllAssets)) { + if (!assetInteraction.selectAll) { assetInteraction.clearMultiselect(); break; // Cancelled } - assetInteraction.selectAssets(assetsSnapshot([...monthGroup.assetsIterator()])); + assetInteraction.selectAssets([...monthGroup.assetsIterator()]); for (const dateGroup of monthGroup.dayGroups) { assetInteraction.addGroupToMultiselectGroup(dateGroup.groupTitle); @@ -450,12 +459,12 @@ export const selectAllAssets = async (timelineManager: TimelineManager, assetInt } catch (error) { const $t = get(t); handleError(error, $t('errors.error_selecting_all_assets')); - isSelectingAllAssets.set(false); + assetInteraction.selectAll = false; } }; export const cancelMultiselect = (assetInteraction: AssetInteraction) => { - isSelectingAllAssets.set(false); + assetInteraction.selectAll = false; assetInteraction.clearMultiselect(); }; diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 1be65638e6..63c7d4e5c8 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -1,10 +1,9 @@ import { browser } from '$app/environment'; import { eventManager } from '$lib/managers/event-manager.svelte'; import { Route } from '$lib/route'; -import { purchaseStore } from '$lib/stores/purchase.store'; import { preferences as preferences$, user as user$ } from '$lib/stores/user.store'; import { userInteraction } from '$lib/stores/user.svelte'; -import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; +import { getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; import { DateTime } from 'luxon'; import { get } from 'svelte/store'; @@ -18,19 +17,12 @@ export const loadUser = async () => { try { let user = get(user$); let preferences = get(preferences$); - let serverInfo; if ((!user || !preferences) && hasAuthCookie()) { - [user, preferences, serverInfo] = await Promise.all([getMyUser(), getMyPreferences(), getAboutInfo()]); + [user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]); user$.set(user); preferences$.set(preferences); - eventManager.emit('AuthUserLoaded', user); - - // Check for license status - if (serverInfo.licensed || user.license?.activatedAt) { - purchaseStore.setPurchaseStatus(true); - } } return user; } catch { diff --git a/web/src/lib/utils/base-event-manager.svelte.ts b/web/src/lib/utils/base-event-manager.svelte.ts index 1b5135dfd9..5112076988 100644 --- a/web/src/lib/utils/base-event-manager.svelte.ts +++ b/web/src/lib/utils/base-event-manager.svelte.ts @@ -15,7 +15,7 @@ const nextId = () => count++; const noop = () => {}; export class BaseEventManager { - #callbacks: EventItem[] = $state([]); + #callbacks: EventItem[] = $state.raw([]); on(subscriptions: EventMap): () => void { const cleanups = Object.entries(subscriptions).map(([event, callback]) => @@ -36,7 +36,7 @@ export class BaseEventManager { // eslint-disable-next-line @typescript-eslint/no-explicit-any const item = { id: nextId(), event, callback } as EventItem; - this.#callbacks.push(item); + this.#callbacks = [...this.#callbacks, item]; return () => { this.#callbacks = this.#callbacks.filter((current) => current.id !== item.id); diff --git a/web/src/lib/utils/context.ts b/web/src/lib/utils/context.ts index 733b8e6eb9..080017a2d3 100644 --- a/web/src/lib/utils/context.ts +++ b/web/src/lib/utils/context.ts @@ -1,3 +1,4 @@ +import type { AssetControlContext } from '$lib/types'; import { getContext, setContext } from 'svelte'; export function createContext(key: string | symbol = Symbol()) { @@ -6,3 +7,5 @@ export function createContext(key: string | symbol = Symbol()) { set: (context: T) => setContext(key, context), }; } + +export const { get: getAssetControlContext, set: setAssetControlContext } = createContext(); diff --git a/web/src/lib/utils/shared-links.ts b/web/src/lib/utils/shared-links.ts index e1bad6bf3a..423eda310c 100644 --- a/web/src/lib/utils/shared-links.ts +++ b/web/src/lib/utils/shared-links.ts @@ -49,7 +49,7 @@ export const loadSharedLink = async ({ }, }; } catch (error) { - if (isHttpError(error) && error.data.message === 'Invalid password') { + if (isHttpError(error) && error.data.message === 'Password required') { return { ...common, passwordRequired: true, diff --git a/web/src/lib/utils/sw-messaging.ts b/web/src/lib/utils/sw-messaging.ts index 61cd1b8df0..3c32bf7de1 100644 --- a/web/src/lib/utils/sw-messaging.ts +++ b/web/src/lib/utils/sw-messaging.ts @@ -1,14 +1,12 @@ -const broadcast = new BroadcastChannel('immich'); +import { ServiceWorkerMessenger } from './sw-messenger'; + +const hasServiceWorker = globalThis.isSecureContext && 'serviceWorker' in navigator; +// eslint-disable-next-line compat/compat +const messenger = hasServiceWorker ? new ServiceWorkerMessenger(navigator.serviceWorker) : undefined; export function cancelImageUrl(url: string | undefined | null) { - if (!url) { + if (!url || !messenger) { return; } - broadcast.postMessage({ type: 'cancel', url }); -} -export function preloadImageUrl(url: string | undefined | null) { - if (!url) { - return; - } - broadcast.postMessage({ type: 'preload', url }); + messenger.send('cancel', { url }); } diff --git a/web/src/lib/utils/sw-messenger.ts b/web/src/lib/utils/sw-messenger.ts new file mode 100644 index 0000000000..b656f3fc2c --- /dev/null +++ b/web/src/lib/utils/sw-messenger.ts @@ -0,0 +1,17 @@ +export class ServiceWorkerMessenger { + readonly #serviceWorker: ServiceWorkerContainer; + + constructor(serviceWorker: ServiceWorkerContainer) { + this.#serviceWorker = serviceWorker; + } + + /** + * Send a one-way message to the service worker. + */ + send(type: string, data: Record) { + this.#serviceWorker.controller?.postMessage({ + type, + ...data, + }); + } +} diff --git a/web/src/routes/(user)/buy/+page.svelte b/web/src/routes/(user)/buy/+page.svelte index 305b994730..111548ba3b 100644 --- a/web/src/routes/(user)/buy/+page.svelte +++ b/web/src/routes/(user)/buy/+page.svelte @@ -4,8 +4,8 @@ import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte'; import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte'; import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte'; + import { authManager } from '$lib/managers/auth-manager.svelte'; import { Route } from '$lib/route'; - import { purchaseStore } from '$lib/stores/purchase.store'; import { Alert, Container, Stack } from '@immich/ui'; import { mdiAlertCircleOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -17,17 +17,16 @@ let { data }: Props = $props(); let showLicenseActivated = $state(false); - const { isPurchased } = purchaseStore; - + {#if data.isActivated === false} {/if} - {#if $isPurchased} + {#if authManager.isPurchased} {/if} diff --git a/web/src/routes/(user)/buy/+page.ts b/web/src/routes/(user)/buy/+page.ts index d0180b39ff..5f937c396e 100644 --- a/web/src/routes/(user)/buy/+page.ts +++ b/web/src/routes/(user)/buy/+page.ts @@ -1,4 +1,4 @@ -import { purchaseStore } from '$lib/stores/purchase.store'; +import { authManager } from '$lib/managers/auth-manager.svelte'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import { activateProduct, getActivationKey } from '$lib/utils/license-utils'; @@ -21,7 +21,7 @@ export const load = (async ({ url }) => { const response = await activateProduct(licenseKey, activationKey); if (response.activatedAt !== '') { isActivated = true; - purchaseStore.setPurchaseStatus(true); + authManager.isPurchased = true; } } } catch (error) { diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 68568cd5b2..9bca4a9094 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,5 +1,6 @@