Compare commits

..

3 Commits

Author SHA1 Message Date
martabal 59300d2097 feat: preload textual model 2024-09-25 18:22:54 +02:00
martabal d34d631dd4 feat: preload textual model 2024-09-25 17:39:55 +02:00
martabal 708a53a1eb feat: preload textual model 2024-09-16 17:53:43 +02:00
127 changed files with 3291 additions and 2992 deletions
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.19",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@@ -52,7 +52,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.115.0",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.19",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
+31 -15
View File
@@ -1,14 +1,18 @@
# External Libraries # Libraries
External libraries track assets stored in the filesystem outside of Immich. When the external library is scanned, Immich will load videos and photos from disk and create the corresponding assets. These assets will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc. Later, if a file is modified outside of Immich, you need to scan the library for the changes to show up. ## Overview
If an external asset is deleted from disk, Immich will move it to trash on rescan. To restore the asset, you need to restore the original file. After 30 days the file will be removed from trash, and any changes to metadata within Immich will be lost. Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
:::caution ## External Libraries
If you add metadata to an external asset in any way (i.e. add it to an album or edit the description), that metadata is only stored inside Immich and will not be persisted to the external asset file. If you move an asset to another location within the library all such metadata will be lost upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release. External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
::: If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case:
- Scan Library Files: This is the default scan method and also the quickest. It will scan all files in the library and add new files to the library. It will notice if any files are missing (see below) but not check existing assets
- Scan All Library Files: Same as above, but will check each existing asset to see if the modification time has changed. If it has, the asset will be updated. Since it has to check each asset, this is slower than Scan Library Files.
- Force Scan All Library Files: Same as above, but will read each asset from disk no matter the modification time. This is useful in some cases where an asset has been modified externally but the modification time has not changed. This is the slowest way to scan because it reads each asset from disk.
:::caution :::caution
@@ -16,6 +20,22 @@ Due to aggressive caching it can take some time for a refreshed asset to appear
::: :::
In external libraries, the file path is used for duplicate detection. This means that if a file is moved to a different location, it will be added as a new asset. If the file is moved back to its original location, it will be added as a new asset. In contrast to upload libraries, two identical files can be uploaded if they are in different locations. This is a deliberate design choice to make Immich reflect the file system as closely as possible. Remember that duplication detection is only done within the same library, so if you have multiple external libraries, the same file can be added to multiple libraries.
:::caution
If you add assets from an external library to an album and then move the asset to another location within the library, the asset will be removed from the album upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release.
:::
### Deleted External Assets
Note: Either a manual or scheduled library scan must have been performed to identify offline assets before this process will work.
In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under Administration > Libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
### Import Paths ### Import Paths
External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. Each import file must be a readable directory that exists on the filesystem; the import path dialog will alert you of any paths that are not accessible. External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. Each import file must be a readable directory that exists on the filesystem; the import path dialog will alert you of any paths that are not accessible.
@@ -46,13 +66,9 @@ Some basic examples:
- `**/Raw/**` will exclude all files in any directory named `Raw` - `**/Raw/**` will exclude all files in any directory named `Raw`
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg` - `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
Special characters such as @ should be escaped, for instance:
- `**/\@eadir/**` will exclude all files in any directory named `@eadir`
### Automatic watching (EXPERIMENTAL) ### Automatic watching (EXPERIMENTAL)
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes. If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
@@ -68,7 +84,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
### Nightly job ### Nightly job
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
## Usage ## Usage
@@ -104,7 +120,7 @@ This will disallow the images from being deleted in the web UI, or adding metada
_Remember to run `docker compose up -d` to register the changes. Make sure you can see the mounted path in the container._ _Remember to run `docker compose up -d` to register the changes. Make sure you can see the mounted path in the container._
::: :::
### Create A New Library ### Create External Libraries
These actions must be performed by the Immich administrator. These actions must be performed by the Immich administrator.
@@ -128,7 +144,7 @@ Next, we'll add an exclusion pattern to filter out raw files.
- Enter `**/Raw/**` and click save. - Enter `**/Raw/**` and click save.
- Click save - Click save
- Click the drop-down menu on the newly created library - Click the drop-down menu on the newly created library
- Click on Scan - Click on Scan Library Files
The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library. The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library.
@@ -145,7 +161,7 @@ If you get an error here, please rename the other external library to something
- Click on Add Path - Click on Add Path
- Enter `/mnt/media/videos` then click Add - Enter `/mnt/media/videos` then click Add
- Click Save - Click Save
- Click on Scan - Click on Scan Library Files
Within seconds, the assets from the old-pics and videos folders should show up in the main timeline. Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.
-13
View File
@@ -6,7 +6,6 @@ import {
mdiLeadPencil, mdiLeadPencil,
mdiLockOff, mdiLockOff,
mdiLockOutline, mdiLockOutline,
mdiMicrosoftWindows,
mdiSecurity, mdiSecurity,
mdiSpeedometerSlow, mdiSpeedometerSlow,
mdiTrashCan, mdiTrashCan,
@@ -22,18 +21,6 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date }; type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [ const items: Item[] = [
{
icon: mdiMicrosoftWindows,
iconColor: '#357EC7',
title: 'Hidden files in Windows are cursed',
description:
'Hidden files in Windows cannot be opened with the "w" flag. That, combined with SMB option "hide dot files" leads to a lot of confusion.',
link: {
url: 'https://github.com/immich-app/immich/pull/12812',
text: '#12812',
},
date: new Date(2024, 8, 20),
},
{ {
icon: mdiWrap, icon: mdiWrap,
iconColor: 'gray', iconColor: 'gray',
-4
View File
@@ -1,8 +1,4 @@
[ [
{
"label": "v1.116.0",
"url": "https://v1.116.0.archive.immich.app"
},
{ {
"label": "v1.115.0", "label": "v1.115.0",
"url": "https://v1.115.0.archive.immich.app" "url": "https://v1.115.0.archive.immich.app"
+4 -4
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.115.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.115.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
@@ -45,7 +45,7 @@
}, },
"../cli": { "../cli": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.19",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -92,7 +92,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.115.0",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.115.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
+346 -148
View File
@@ -1,4 +1,11 @@
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk'; import {
LibraryResponseDto,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
removeOfflineFiles,
scanLibrary,
} from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs'; import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures'; import { userDto, uuidDto } from 'src/fixtures';
@@ -8,7 +15,8 @@ import request from 'supertest';
import { utimes } from 'utimes'; import { utimes } from 'utimes';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) }); const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/libraries', () => { describe('/libraries', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
@@ -285,19 +293,14 @@ describe('/libraries', () => {
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should import new asset when scanning external library', async () => { it('should scan external library', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/directoryA`], importPaths: [`${testAssetDirInternal}/temp/directoryA`],
}); });
const { status } = await request(app) await scan(admin.accessToken, library.id);
.post(`/libraries/${library.id}/scan`) await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 });
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { const { assets } = await utils.metadataSearch(admin.accessToken, {
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`, originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
@@ -312,13 +315,8 @@ describe('/libraries', () => {
exclusionPatterns: ['**/directoryA'], exclusionPatterns: ['**/directoryA'],
}); });
const { status } = await request(app) await scan(admin.accessToken, library.id);
.post(`/libraries/${library.id}/scan`) await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 });
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
@@ -332,13 +330,8 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`], importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
}); });
const { status } = await request(app) await scan(admin.accessToken, library.id);
.post(`/libraries/${library.id}/scan`) await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
@@ -347,144 +340,95 @@ describe('/libraries', () => {
expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).toBeDefined(); expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).toBeDefined();
}); });
it('should reimport a modified file', async () => { it('should pick up new files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true });
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true });
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(0);
});
it('should set an asset offline if its file is missing', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); expect(assets.count).toBe(2);
const { status } = await request(app) utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 });
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(trashedAsset.isOffline).toEqual(true);
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([]);
expect(newAssets.count).toBe(3);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
}); });
it('should set an asset offline its file is not in any import path', async () => { it('should offline a file missing from disk', async () => {
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1); expect(assets.count).toBe(3);
utils.createDirectory(`${testAssetDir}/temp/another-path/`); utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.count).toBe(3);
expect(newAssets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: true,
originalFileName: 'assetC.png',
}),
]),
);
});
it('should offline a file outside of import paths', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await request(app) await request(app)
.put(`/libraries/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [`${testAssetDirInternal}/temp/another-path/`] }); .send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] });
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(trashedAsset.isOffline).toBe(true);
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); expect(assets.items).toEqual(
expect.arrayContaining([
expect(newAssets.items).toEqual([]); expect.objectContaining({
isOffline: false,
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); originalFileName: 'assetA.png',
utils.removeDirectory(`${testAssetDir}/temp/another-path/`); }),
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
}),
]),
);
}); });
it('should set an asset offline if its file is covered by an exclusion pattern', async () => { it('should offline a file covered by an exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
@@ -493,12 +437,6 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
originalFileName: 'assetB.png',
});
expect(assets.count).toBe(1);
await request(app) await request(app)
.put(`/libraries/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
@@ -507,21 +445,282 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(trashedAsset.isTrashed).toBe(true);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/directoryB/assetB.png`);
expect(trashedAsset.isOffline).toBe(true);
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); expect(assets.count).toBe(2);
expect(newAssets.items).toEqual([ expect(assets.items).toEqual(
expect.objectContaining({ expect.arrayContaining([
originalFileName: 'assetA.png', expect.objectContaining({
}), isOffline: false,
]); originalFileName: 'assetA.png',
}),
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
}),
]),
);
}); });
it('should not trash an online asset', async () => { it('should not try to delete offline files', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline1`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(initialAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
});
it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(3);
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
originalFileName: 'assetC.png',
}),
]),
);
utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
});
describe('with refreshModifiedFiles=true', () => {
it('should reimport modified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(0);
});
});
describe('with refreshAllFiles=true', () => {
it('should reimport all files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id, { refreshAllFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
});
});
describe('POST /libraries/:id/removeOffline', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(2);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets.count).toBe(1);
const { status } = await request(app)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
});
it('should remove offline files from trash', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(2);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets.count).toBe(1);
const { status } = await request(app)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
expect(assets.items[0].isOffline).toBe(false);
expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`);
utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
});
it('should not remove online files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
@@ -534,11 +733,10 @@ describe('/libraries', () => {
expect(assetsBefore.count).toBeGreaterThan(1); expect(assetsBefore.count).toBeGreaterThan(1);
const { status } = await request(app) const { status } = await request(app)
.post(`/libraries/${library.id}/scan`) .post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send(); .send();
expect(status).toBe(204); expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
@@ -630,7 +828,7 @@ describe('/libraries', () => {
}); });
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/libraries/${library.id}`) .delete(`/libraries/${library.id}`)
+1 -1
View File
@@ -181,7 +181,7 @@ describe('/search', () => {
dto: { size: -1.5 }, dto: { size: -1.5 },
expected: ['size must not be less than 1', 'size must be an integer number'], expected: ['size must not be less than 1', 'size must be an integer number'],
}, },
...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({ ...['isArchived', 'isFavorite', 'isEncoded', 'isMotion', 'isOffline', 'isVisible'].map((value) => ({
should: `should reject ${value} not a boolean`, should: `should reject ${value} not a boolean`,
dto: { [value]: 'immich' }, dto: { [value]: 'immich' },
expected: [`${value} must be a boolean value`], expected: [`${value} must be a boolean value`],
+2 -111
View File
@@ -1,13 +1,10 @@
import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk'; import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
import { existsSync } from 'node:fs';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
describe('/trash', () => { describe('/trash', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let ws: Socket; let ws: Socket;
@@ -47,8 +44,6 @@ describe('/trash', () => {
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0); expect(after.total).toBe(0);
expect(existsSync(before.originalPath)).toBe(false);
}); });
it('should empty the trash with archived assets', async () => { it('should empty the trash with archived assets', async () => {
@@ -69,46 +64,6 @@ describe('/trash', () => {
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0); expect(after.total).toBe(0);
expect(existsSync(before.originalPath)).toBe(false);
});
it('should not delete offline-trashed assets from disk', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.items.length).toBe(1);
const asset = assets.items[0];
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const assetAfter = await utils.getAssetInfo(admin.accessToken, asset.id);
expect(assetAfter).toMatchObject({ isTrashed: true, isOffline: true });
expect(existsSync(`${testAssetDir}/temp/offline/offline.png`)).toBe(true);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
}); });
}); });
@@ -136,37 +91,6 @@ describe('/trash', () => {
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false })); expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
}); });
it('should not restore offline-trashed assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
const assetId = assets.items[0].id;
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
});
}); });
describe('POST /trash/restore/assets', () => { describe('POST /trash/restore/assets', () => {
@@ -194,38 +118,5 @@ describe('/trash', () => {
const after = await utils.getAssetInfo(admin.accessToken, assetId); const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(false); expect(after.isTrashed).toBe(false);
}); });
it('should not restore an offline-trashed asset', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
const assetId = assets.items[0].id;
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true);
const { status } = await request(app)
.post('/trash/restore/assets')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [assetId] });
expect(status).toBe(200);
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(true);
});
}); });
}); });
-14
View File
@@ -372,12 +372,6 @@ export const utils = {
writeFileSync(path, makeRandomImage()); writeFileSync(path, makeRandomImage());
}, },
createDirectory: (path: string) => {
if (!existsSync(dirname(path))) {
mkdirSync(dirname(path), { recursive: true });
}
},
removeImageFile: (path: string) => { removeImageFile: (path: string) => {
if (!existsSync(path)) { if (!existsSync(path)) {
return; return;
@@ -386,14 +380,6 @@ export const utils = {
rmSync(path); rmSync(path);
}, },
removeDirectory: (path: string) => {
if (!existsSync(path)) {
return;
}
rmSync(path);
},
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) => checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
+32 -1
View File
@@ -11,7 +11,7 @@ from typing import Any, AsyncGenerator, Callable, Iterator
from zipfile import BadZipFile from zipfile import BadZipFile
import orjson import orjson
from fastapi import Depends, FastAPI, File, Form, HTTPException from fastapi import Depends, FastAPI, File, Form, HTTPException, Response
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
from PIL.Image import Image from PIL.Image import Image
@@ -124,6 +124,23 @@ def get_entries(entries: str = Form()) -> InferenceEntries:
raise HTTPException(422, "Invalid request format.") raise HTTPException(422, "Invalid request format.")
def get_entry(entries: str = Form()) -> InferenceEntry:
try:
request: PipelineRequest = orjson.loads(entries)
for task, types in request.items():
for type, entry in types.items():
parsed: InferenceEntry = {
"name": entry["modelName"],
"task": task,
"type": type,
"options": entry.get("options", {}),
}
return parsed
except (orjson.JSONDecodeError, ValidationError, KeyError, AttributeError) as e:
log.error(f"Invalid request format: {e}")
raise HTTPException(422, "Invalid request format.")
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
@@ -137,6 +154,20 @@ def ping() -> str:
return "pong" return "pong"
@app.post("/load", response_model=TextResponse)
async def load_model(entry: InferenceEntry = Depends(get_entry)) -> None:
model = await model_cache.get(entry["name"], entry["type"], entry["task"], ttl=settings.model_ttl)
model = await load(model)
return Response(status_code=200)
@app.post("/unload", response_model=TextResponse)
async def unload_model(entry: InferenceEntry = Depends(get_entry)) -> None:
await model_cache.unload(entry["name"], entry["type"], entry["task"])
print("unload")
return Response(status_code=200)
@app.post("/predict", dependencies=[Depends(update_state)]) @app.post("/predict", dependencies=[Depends(update_state)])
async def predict( async def predict(
entries: InferenceEntries = Depends(get_entries), entries: InferenceEntries = Depends(get_entries),
+7
View File
@@ -58,3 +58,10 @@ class ModelCache:
async def revalidate(self, key: str, ttl: int | None) -> None: async def revalidate(self, key: str, ttl: int | None) -> None:
if ttl is not None and key in self.cache._handlers: if ttl is not None and key in self.cache._handlers:
await self.cache.expire(key, ttl) await self.cache.expire(key, ttl)
async def unload(self, model_name: str, model_type: ModelType, model_task: ModelTask) -> None:
key = f"{model_name}{model_type}{model_task}"
async with OptimisticLock(self.cache, key):
value = await self.cache.get(key)
if value is not None:
await self.cache.delete(key)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.116.0" version = "1.115.0"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 160, "android.injected.version.code" => 159,
"android.injected.version.name" => "1.116.0", "android.injected.version.name" => "1.115.0",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release" desc "iOS Release"
lane :release do lane :release do
increment_version_number( increment_version_number(
version_number: "1.116.0" version_number: "1.115.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
+98 -43
View File
@@ -57,64 +57,69 @@ const AssetSchema = CollectionSchema(
name: r'isFavorite', name: r'isFavorite',
type: IsarType.bool, type: IsarType.bool,
), ),
r'isTrashed': PropertySchema( r'isOffline': PropertySchema(
id: 8, id: 8,
name: r'isOffline',
type: IsarType.bool,
),
r'isTrashed': PropertySchema(
id: 9,
name: r'isTrashed', name: r'isTrashed',
type: IsarType.bool, type: IsarType.bool,
), ),
r'livePhotoVideoId': PropertySchema( r'livePhotoVideoId': PropertySchema(
id: 9, id: 10,
name: r'livePhotoVideoId', name: r'livePhotoVideoId',
type: IsarType.string, type: IsarType.string,
), ),
r'localId': PropertySchema( r'localId': PropertySchema(
id: 10, id: 11,
name: r'localId', name: r'localId',
type: IsarType.string, type: IsarType.string,
), ),
r'ownerId': PropertySchema( r'ownerId': PropertySchema(
id: 11, id: 12,
name: r'ownerId', name: r'ownerId',
type: IsarType.long, type: IsarType.long,
), ),
r'remoteId': PropertySchema( r'remoteId': PropertySchema(
id: 12, id: 13,
name: r'remoteId', name: r'remoteId',
type: IsarType.string, type: IsarType.string,
), ),
r'stackCount': PropertySchema( r'stackCount': PropertySchema(
id: 13, id: 14,
name: r'stackCount', name: r'stackCount',
type: IsarType.long, type: IsarType.long,
), ),
r'stackId': PropertySchema( r'stackId': PropertySchema(
id: 14, id: 15,
name: r'stackId', name: r'stackId',
type: IsarType.string, type: IsarType.string,
), ),
r'stackPrimaryAssetId': PropertySchema( r'stackPrimaryAssetId': PropertySchema(
id: 15, id: 16,
name: r'stackPrimaryAssetId', name: r'stackPrimaryAssetId',
type: IsarType.string, type: IsarType.string,
), ),
r'thumbhash': PropertySchema( r'thumbhash': PropertySchema(
id: 16, id: 17,
name: r'thumbhash', name: r'thumbhash',
type: IsarType.string, type: IsarType.string,
), ),
r'type': PropertySchema( r'type': PropertySchema(
id: 17, id: 18,
name: r'type', name: r'type',
type: IsarType.byte, type: IsarType.byte,
enumMap: _AssettypeEnumValueMap, enumMap: _AssettypeEnumValueMap,
), ),
r'updatedAt': PropertySchema( r'updatedAt': PropertySchema(
id: 18, id: 19,
name: r'updatedAt', name: r'updatedAt',
type: IsarType.dateTime, type: IsarType.dateTime,
), ),
r'width': PropertySchema( r'width': PropertySchema(
id: 19, id: 20,
name: r'width', name: r'width',
type: IsarType.int, type: IsarType.int,
) )
@@ -239,18 +244,19 @@ void _assetSerialize(
writer.writeInt(offsets[5], object.height); writer.writeInt(offsets[5], object.height);
writer.writeBool(offsets[6], object.isArchived); writer.writeBool(offsets[6], object.isArchived);
writer.writeBool(offsets[7], object.isFavorite); writer.writeBool(offsets[7], object.isFavorite);
writer.writeBool(offsets[8], object.isTrashed); writer.writeBool(offsets[8], object.isOffline);
writer.writeString(offsets[9], object.livePhotoVideoId); writer.writeBool(offsets[9], object.isTrashed);
writer.writeString(offsets[10], object.localId); writer.writeString(offsets[10], object.livePhotoVideoId);
writer.writeLong(offsets[11], object.ownerId); writer.writeString(offsets[11], object.localId);
writer.writeString(offsets[12], object.remoteId); writer.writeLong(offsets[12], object.ownerId);
writer.writeLong(offsets[13], object.stackCount); writer.writeString(offsets[13], object.remoteId);
writer.writeString(offsets[14], object.stackId); writer.writeLong(offsets[14], object.stackCount);
writer.writeString(offsets[15], object.stackPrimaryAssetId); writer.writeString(offsets[15], object.stackId);
writer.writeString(offsets[16], object.thumbhash); writer.writeString(offsets[16], object.stackPrimaryAssetId);
writer.writeByte(offsets[17], object.type.index); writer.writeString(offsets[17], object.thumbhash);
writer.writeDateTime(offsets[18], object.updatedAt); writer.writeByte(offsets[18], object.type.index);
writer.writeInt(offsets[19], object.width); writer.writeDateTime(offsets[19], object.updatedAt);
writer.writeInt(offsets[20], object.width);
} }
Asset _assetDeserialize( Asset _assetDeserialize(
@@ -269,19 +275,20 @@ Asset _assetDeserialize(
id: id, id: id,
isArchived: reader.readBoolOrNull(offsets[6]) ?? false, isArchived: reader.readBoolOrNull(offsets[6]) ?? false,
isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, isFavorite: reader.readBoolOrNull(offsets[7]) ?? false,
isTrashed: reader.readBoolOrNull(offsets[8]) ?? false, isOffline: reader.readBoolOrNull(offsets[8]) ?? false,
livePhotoVideoId: reader.readStringOrNull(offsets[9]), isTrashed: reader.readBoolOrNull(offsets[9]) ?? false,
localId: reader.readStringOrNull(offsets[10]), livePhotoVideoId: reader.readStringOrNull(offsets[10]),
ownerId: reader.readLong(offsets[11]), localId: reader.readStringOrNull(offsets[11]),
remoteId: reader.readStringOrNull(offsets[12]), ownerId: reader.readLong(offsets[12]),
stackCount: reader.readLongOrNull(offsets[13]) ?? 0, remoteId: reader.readStringOrNull(offsets[13]),
stackId: reader.readStringOrNull(offsets[14]), stackCount: reader.readLongOrNull(offsets[14]) ?? 0,
stackPrimaryAssetId: reader.readStringOrNull(offsets[15]), stackId: reader.readStringOrNull(offsets[15]),
thumbhash: reader.readStringOrNull(offsets[16]), stackPrimaryAssetId: reader.readStringOrNull(offsets[16]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ?? thumbhash: reader.readStringOrNull(offsets[17]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
AssetType.other, AssetType.other,
updatedAt: reader.readDateTime(offsets[18]), updatedAt: reader.readDateTime(offsets[19]),
width: reader.readIntOrNull(offsets[19]), width: reader.readIntOrNull(offsets[20]),
); );
return object; return object;
} }
@@ -312,27 +319,29 @@ P _assetDeserializeProp<P>(
case 8: case 8:
return (reader.readBoolOrNull(offset) ?? false) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 9: case 9:
return (reader.readStringOrNull(offset)) as P; return (reader.readBoolOrNull(offset) ?? false) as P;
case 10: case 10:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 11: case 11:
return (reader.readLong(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 12: case 12:
return (reader.readStringOrNull(offset)) as P; return (reader.readLong(offset)) as P;
case 13: case 13:
return (reader.readLongOrNull(offset) ?? 0) as P;
case 14:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 14:
return (reader.readLongOrNull(offset) ?? 0) as P;
case 15: case 15:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 16: case 16:
return (reader.readStringOrNull(offset)) as P; return (reader.readStringOrNull(offset)) as P;
case 17: case 17:
return (reader.readStringOrNull(offset)) as P;
case 18:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P; AssetType.other) as P;
case 18:
return (reader.readDateTime(offset)) as P;
case 19: case 19:
return (reader.readDateTime(offset)) as P;
case 20:
return (reader.readIntOrNull(offset)) as P; return (reader.readIntOrNull(offset)) as P;
default: default:
throw IsarError('Unknown property with id $propertyId'); throw IsarError('Unknown property with id $propertyId');
@@ -1353,6 +1362,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
}); });
} }
QueryBuilder<Asset, Asset, QAfterFilterCondition> isOfflineEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isOffline',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo( QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
bool value) { bool value) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
@@ -2628,6 +2647,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
}); });
} }
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOfflineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() { QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc); return query.addSortBy(r'isTrashed', Sort.asc);
@@ -2882,6 +2913,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
}); });
} }
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOfflineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() { QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc); return query.addSortBy(r'isTrashed', Sort.asc);
@@ -3078,6 +3121,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
}); });
} }
QueryBuilder<Asset, Asset, QDistinct> distinctByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isOffline');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() { QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isTrashed'); return query.addDistinctBy(r'isTrashed');
@@ -3214,6 +3263,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
}); });
} }
QueryBuilder<Asset, bool, QQueryOperations> isOfflineProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isOffline');
});
}
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() { QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isTrashed'); return query.addPropertyName(r'isTrashed');
@@ -72,14 +72,13 @@ extension AssetListExtension on Iterable<Asset> {
} }
/// Filters out offline assets and returns those that are still accessible by the Immich server /// Filters out offline assets and returns those that are still accessible by the Immich server
/// TODO: isOffline is removed from Immich, so this method is not useful anymore
Iterable<Asset> nonOfflineOnly({ Iterable<Asset> nonOfflineOnly({
void Function()? errorCallback, void Function()? errorCallback,
}) { }) {
final bool onlyLive = every((e) => false); final bool onlyLive = every((e) => !e.isOffline);
if (!onlyLive) { if (!onlyLive) {
if (errorCallback != null) errorCallback(); if (errorCallback != null) errorCallback();
return where((a) => false); return where((a) => !a.isOffline);
} }
return this; return this;
} }
-16
View File
@@ -1,16 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/repositories/activity_api.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/person_api.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.dart';
void invalidateAllApiRepositoryProviders(WidgetRef ref) {
ref.invalidate(userApiRepositoryProvider);
ref.invalidate(activityApiRepositoryProvider);
ref.invalidate(partnerApiRepositoryProvider);
ref.invalidate(albumApiRepositoryProvider);
ref.invalidate(personApiRepositoryProvider);
ref.invalidate(assetApiRepositoryProvider);
}
@@ -172,12 +172,29 @@ class BottomGalleryBar extends ConsumerWidget {
} }
shareAsset() { shareAsset() {
if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context); ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
} }
void handleEdit() async { void handleEdit() async {
final image = Image(image: ImmichImage.imageProvider(asset: asset)); final image = Image(image: ImmichImage.imageProvider(asset: asset));
if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_edit_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => EditImagePage( builder: (context) => EditImagePage(
@@ -202,6 +219,16 @@ class BottomGalleryBar extends ConsumerWidget {
if (asset.isLocal) { if (asset.isLocal) {
return; return;
} }
if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(imageViewerStateProvider.notifier).downloadAsset( ref.read(imageViewerStateProvider.notifier).downloadAsset(
asset, asset,
context, context,
@@ -183,7 +183,8 @@ class TopControlAppBar extends HookConsumerWidget {
if (asset.isRemote && isOwner) buildFavoriteButton(a), if (asset.isRemote && isOwner) buildFavoriteButton(a),
if (asset.livePhotoVideoId != null) buildLivePhotoButton(), if (asset.livePhotoVideoId != null) buildLivePhotoButton(),
if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(),
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), if (asset.isRemote && !asset.isLocal && !asset.isOffline && isOwner)
buildDownloadButton(),
if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed) if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed)
buildAddToAlbumButton(), buildAddToAlbumButton(),
if (asset.isTrashed) buildRestoreButton(), if (asset.isTrashed) buildRestoreButton(),
@@ -16,7 +16,6 @@ import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/authentication.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/provider_utils.dart';
import 'package:immich_mobile/utils/version_compatibility.dart'; import 'package:immich_mobile/utils/version_compatibility.dart';
import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart';
import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart';
@@ -187,9 +186,6 @@ class LoginForm extends HookConsumerWidget {
// This will remove current cache asset state of previous user login. // This will remove current cache asset state of previous user login.
ref.read(assetProvider.notifier).clearAllAsset(); ref.read(assetProvider.notifier).clearAllAsset();
// Invalidate all api repository provider instance to take into account new access token
invalidateAllApiRepositoryProviders(ref);
try { try {
final isAuthenticated = final isAuthenticated =
await ref.read(authenticationProvider.notifier).login( await ref.read(authenticationProvider.notifier).login(
+6 -9
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.116.0 - API version: 1.115.0
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -116,7 +116,6 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets | *DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets |
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
@@ -125,7 +124,6 @@ Class | Method | HTTP request | Description
*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix | *FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix |
*FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports | *FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports |
*FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum | *FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum |
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs |
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs | *JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} | *JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries | *LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries |
@@ -133,10 +131,12 @@ Class | Method | HTTP request | Description
*LibrariesApi* | [**getAllLibraries**](doc//LibrariesApi.md#getalllibraries) | **GET** /libraries | *LibrariesApi* | [**getAllLibraries**](doc//LibrariesApi.md#getalllibraries) | **GET** /libraries |
*LibrariesApi* | [**getLibrary**](doc//LibrariesApi.md#getlibrary) | **GET** /libraries/{id} | *LibrariesApi* | [**getLibrary**](doc//LibrariesApi.md#getlibrary) | **GET** /libraries/{id} |
*LibrariesApi* | [**getLibraryStatistics**](doc//LibrariesApi.md#getlibrarystatistics) | **GET** /libraries/{id}/statistics | *LibrariesApi* | [**getLibraryStatistics**](doc//LibrariesApi.md#getlibrarystatistics) | **GET** /libraries/{id}/statistics |
*LibrariesApi* | [**removeOfflineFiles**](doc//LibrariesApi.md#removeofflinefiles) | **POST** /libraries/{id}/removeOffline |
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | *LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan |
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} |
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |
*MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json |
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | *MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode |
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | *MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
*MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories | *MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |
@@ -171,7 +171,6 @@ Class | Method | HTTP request | Description
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata | *SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random |
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
*ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license |
*ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | *ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about |
@@ -331,7 +330,6 @@ Class | Method | HTTP request | Description
- [JobCommand](doc//JobCommand.md) - [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md) - [JobCommandDto](doc//JobCommandDto.md)
- [JobCountsDto](doc//JobCountsDto.md) - [JobCountsDto](doc//JobCountsDto.md)
- [JobCreateDto](doc//JobCreateDto.md)
- [JobName](doc//JobName.md) - [JobName](doc//JobName.md)
- [JobSettingsDto](doc//JobSettingsDto.md) - [JobSettingsDto](doc//JobSettingsDto.md)
- [JobStatusDto](doc//JobStatusDto.md) - [JobStatusDto](doc//JobStatusDto.md)
@@ -339,13 +337,14 @@ Class | Method | HTTP request | Description
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
- [LicenseKeyDto](doc//LicenseKeyDto.md) - [LicenseKeyDto](doc//LicenseKeyDto.md)
- [LicenseResponseDto](doc//LicenseResponseDto.md) - [LicenseResponseDto](doc//LicenseResponseDto.md)
- [LoadTextualModelOnConnection](doc//LoadTextualModelOnConnection.md)
- [LogLevel](doc//LogLevel.md) - [LogLevel](doc//LogLevel.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md) - [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md)
- [ManualJobName](doc//ManualJobName.md)
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
- [MapTheme](doc//MapTheme.md)
- [MemoriesResponse](doc//MemoriesResponse.md) - [MemoriesResponse](doc//MemoriesResponse.md)
- [MemoriesUpdate](doc//MemoriesUpdate.md) - [MemoriesUpdate](doc//MemoriesUpdate.md)
- [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryCreateDto](doc//MemoryCreateDto.md)
@@ -378,12 +377,12 @@ Class | Method | HTTP request | Description
- [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseResponse](doc//PurchaseResponse.md)
- [PurchaseUpdate](doc//PurchaseUpdate.md) - [PurchaseUpdate](doc//PurchaseUpdate.md)
- [QueueStatusDto](doc//QueueStatusDto.md) - [QueueStatusDto](doc//QueueStatusDto.md)
- [RandomSearchDto](doc//RandomSearchDto.md)
- [RatingsResponse](doc//RatingsResponse.md) - [RatingsResponse](doc//RatingsResponse.md)
- [RatingsUpdate](doc//RatingsUpdate.md) - [RatingsUpdate](doc//RatingsUpdate.md)
- [ReactionLevel](doc//ReactionLevel.md) - [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md) - [ReactionType](doc//ReactionType.md)
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
- [ScanLibraryDto](doc//ScanLibraryDto.md)
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
- [SearchExploreItem](doc//SearchExploreItem.md) - [SearchExploreItem](doc//SearchExploreItem.md)
@@ -446,13 +445,11 @@ Class | Method | HTTP request | Description
- [TagUpsertDto](doc//TagUpsertDto.md) - [TagUpsertDto](doc//TagUpsertDto.md)
- [TagsResponse](doc//TagsResponse.md) - [TagsResponse](doc//TagsResponse.md)
- [TagsUpdate](doc//TagsUpdate.md) - [TagsUpdate](doc//TagsUpdate.md)
- [TestEmailResponseDto](doc//TestEmailResponseDto.md)
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
- [TimeBucketSize](doc//TimeBucketSize.md) - [TimeBucketSize](doc//TimeBucketSize.md)
- [ToneMapping](doc//ToneMapping.md) - [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md) - [TranscodePolicy](doc//TranscodePolicy.md)
- [TrashResponseDto](doc//TrashResponseDto.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md)
+3 -5
View File
@@ -144,7 +144,6 @@ part 'model/image_format.dart';
part 'model/job_command.dart'; part 'model/job_command.dart';
part 'model/job_command_dto.dart'; part 'model/job_command_dto.dart';
part 'model/job_counts_dto.dart'; part 'model/job_counts_dto.dart';
part 'model/job_create_dto.dart';
part 'model/job_name.dart'; part 'model/job_name.dart';
part 'model/job_settings_dto.dart'; part 'model/job_settings_dto.dart';
part 'model/job_status_dto.dart'; part 'model/job_status_dto.dart';
@@ -152,13 +151,14 @@ part 'model/library_response_dto.dart';
part 'model/library_stats_response_dto.dart'; part 'model/library_stats_response_dto.dart';
part 'model/license_key_dto.dart'; part 'model/license_key_dto.dart';
part 'model/license_response_dto.dart'; part 'model/license_response_dto.dart';
part 'model/load_textual_model_on_connection.dart';
part 'model/log_level.dart'; part 'model/log_level.dart';
part 'model/login_credential_dto.dart'; part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart'; part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart'; part 'model/logout_response_dto.dart';
part 'model/manual_job_name.dart';
part 'model/map_marker_response_dto.dart'; part 'model/map_marker_response_dto.dart';
part 'model/map_reverse_geocode_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart';
part 'model/map_theme.dart';
part 'model/memories_response.dart'; part 'model/memories_response.dart';
part 'model/memories_update.dart'; part 'model/memories_update.dart';
part 'model/memory_create_dto.dart'; part 'model/memory_create_dto.dart';
@@ -191,12 +191,12 @@ part 'model/places_response_dto.dart';
part 'model/purchase_response.dart'; part 'model/purchase_response.dart';
part 'model/purchase_update.dart'; part 'model/purchase_update.dart';
part 'model/queue_status_dto.dart'; part 'model/queue_status_dto.dart';
part 'model/random_search_dto.dart';
part 'model/ratings_response.dart'; part 'model/ratings_response.dart';
part 'model/ratings_update.dart'; part 'model/ratings_update.dart';
part 'model/reaction_level.dart'; part 'model/reaction_level.dart';
part 'model/reaction_type.dart'; part 'model/reaction_type.dart';
part 'model/reverse_geocoding_state_response_dto.dart'; part 'model/reverse_geocoding_state_response_dto.dart';
part 'model/scan_library_dto.dart';
part 'model/search_album_response_dto.dart'; part 'model/search_album_response_dto.dart';
part 'model/search_asset_response_dto.dart'; part 'model/search_asset_response_dto.dart';
part 'model/search_explore_item.dart'; part 'model/search_explore_item.dart';
@@ -259,13 +259,11 @@ part 'model/tag_update_dto.dart';
part 'model/tag_upsert_dto.dart'; part 'model/tag_upsert_dto.dart';
part 'model/tags_response.dart'; part 'model/tags_response.dart';
part 'model/tags_update.dart'; part 'model/tags_update.dart';
part 'model/test_email_response_dto.dart';
part 'model/time_bucket_response_dto.dart'; part 'model/time_bucket_response_dto.dart';
part 'model/time_bucket_size.dart'; part 'model/time_bucket_size.dart';
part 'model/tone_mapping.dart'; part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart'; part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart'; part 'model/transcode_policy.dart';
part 'model/trash_response_dto.dart';
part 'model/update_album_dto.dart'; part 'model/update_album_dto.dart';
part 'model/update_album_user_dto.dart'; part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart'; part 'model/update_asset_dto.dart';
+11 -3
View File
@@ -833,12 +833,14 @@ class AssetsApi {
/// ///
/// * [bool] isFavorite: /// * [bool] isFavorite:
/// ///
/// * [bool] isOffline:
///
/// * [bool] isVisible: /// * [bool] isVisible:
/// ///
/// * [String] livePhotoVideoId: /// * [String] livePhotoVideoId:
/// ///
/// * [MultipartFile] sidecarData: /// * [MultipartFile] sidecarData:
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isOffline, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/assets'; final path = r'/assets';
@@ -894,6 +896,10 @@ class AssetsApi {
hasFields = true; hasFields = true;
mp.fields[r'isFavorite'] = parameterToString(isFavorite); mp.fields[r'isFavorite'] = parameterToString(isFavorite);
} }
if (isOffline != null) {
hasFields = true;
mp.fields[r'isOffline'] = parameterToString(isOffline);
}
if (isVisible != null) { if (isVisible != null) {
hasFields = true; hasFields = true;
mp.fields[r'isVisible'] = parameterToString(isVisible); mp.fields[r'isVisible'] = parameterToString(isVisible);
@@ -945,13 +951,15 @@ class AssetsApi {
/// ///
/// * [bool] isFavorite: /// * [bool] isFavorite:
/// ///
/// * [bool] isOffline:
///
/// * [bool] isVisible: /// * [bool] isVisible:
/// ///
/// * [String] livePhotoVideoId: /// * [String] livePhotoVideoId:
/// ///
/// * [MultipartFile] sidecarData: /// * [MultipartFile] sidecarData:
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isOffline, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async {
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isVisible: isVisible, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, ); final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isOffline: isOffline, isVisible: isVisible, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
+49 -5
View File
@@ -243,13 +243,13 @@ class LibrariesApi {
return null; return null;
} }
/// Performs an HTTP 'POST /libraries/{id}/scan' operation and returns the [Response]. /// Performs an HTTP 'POST /libraries/{id}/removeOffline' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
Future<Response> scanLibraryWithHttpInfo(String id,) async { Future<Response> removeOfflineFilesWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/libraries/{id}/scan' final path = r'/libraries/{id}/removeOffline'
.replaceAll('{id}', id); .replaceAll('{id}', id);
// ignore: prefer_final_locals // ignore: prefer_final_locals
@@ -276,8 +276,52 @@ class LibrariesApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
Future<void> scanLibrary(String id,) async { Future<void> removeOfflineFiles(String id,) async {
final response = await scanLibraryWithHttpInfo(id,); final response = await removeOfflineFilesWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /libraries/{id}/scan' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [ScanLibraryDto] scanLibraryDto (required):
Future<Response> scanLibraryWithHttpInfo(String id, ScanLibraryDto scanLibraryDto,) async {
// ignore: prefer_const_declarations
final path = r'/libraries/{id}/scan'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = scanLibraryDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [ScanLibraryDto] scanLibraryDto (required):
Future<void> scanLibrary(String id, ScanLibraryDto scanLibraryDto,) async {
final response = await scanLibraryWithHttpInfo(id, scanLibraryDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
+1 -9
View File
@@ -48,18 +48,10 @@ class NotificationsApi {
/// Parameters: /// Parameters:
/// ///
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
Future<TestEmailResponseDto?> sendTestEmail(SystemConfigSmtpDto systemConfigSmtpDto,) async { Future<void> sendTestEmail(SystemConfigSmtpDto systemConfigSmtpDto,) async {
final response = await sendTestEmailWithHttpInfo(systemConfigSmtpDto,); final response = await sendTestEmailWithHttpInfo(systemConfigSmtpDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); 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), 'TestEmailResponseDto',) as TestEmailResponseDto;
}
return null;
} }
} }
+7 -10
View File
@@ -166,6 +166,7 @@ class ApiClient {
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. /// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
upgradeDto(value, targetType);
try { try {
switch (targetType) { switch (targetType) {
case 'String': case 'String':
@@ -342,8 +343,6 @@ class ApiClient {
return JobCommandDto.fromJson(value); return JobCommandDto.fromJson(value);
case 'JobCountsDto': case 'JobCountsDto':
return JobCountsDto.fromJson(value); return JobCountsDto.fromJson(value);
case 'JobCreateDto':
return JobCreateDto.fromJson(value);
case 'JobName': case 'JobName':
return JobNameTypeTransformer().decode(value); return JobNameTypeTransformer().decode(value);
case 'JobSettingsDto': case 'JobSettingsDto':
@@ -358,6 +357,8 @@ class ApiClient {
return LicenseKeyDto.fromJson(value); return LicenseKeyDto.fromJson(value);
case 'LicenseResponseDto': case 'LicenseResponseDto':
return LicenseResponseDto.fromJson(value); return LicenseResponseDto.fromJson(value);
case 'LoadTextualModelOnConnection':
return LoadTextualModelOnConnection.fromJson(value);
case 'LogLevel': case 'LogLevel':
return LogLevelTypeTransformer().decode(value); return LogLevelTypeTransformer().decode(value);
case 'LoginCredentialDto': case 'LoginCredentialDto':
@@ -366,12 +367,12 @@ class ApiClient {
return LoginResponseDto.fromJson(value); return LoginResponseDto.fromJson(value);
case 'LogoutResponseDto': case 'LogoutResponseDto':
return LogoutResponseDto.fromJson(value); return LogoutResponseDto.fromJson(value);
case 'ManualJobName':
return ManualJobNameTypeTransformer().decode(value);
case 'MapMarkerResponseDto': case 'MapMarkerResponseDto':
return MapMarkerResponseDto.fromJson(value); return MapMarkerResponseDto.fromJson(value);
case 'MapReverseGeocodeResponseDto': case 'MapReverseGeocodeResponseDto':
return MapReverseGeocodeResponseDto.fromJson(value); return MapReverseGeocodeResponseDto.fromJson(value);
case 'MapTheme':
return MapThemeTypeTransformer().decode(value);
case 'MemoriesResponse': case 'MemoriesResponse':
return MemoriesResponse.fromJson(value); return MemoriesResponse.fromJson(value);
case 'MemoriesUpdate': case 'MemoriesUpdate':
@@ -436,8 +437,6 @@ class ApiClient {
return PurchaseUpdate.fromJson(value); return PurchaseUpdate.fromJson(value);
case 'QueueStatusDto': case 'QueueStatusDto':
return QueueStatusDto.fromJson(value); return QueueStatusDto.fromJson(value);
case 'RandomSearchDto':
return RandomSearchDto.fromJson(value);
case 'RatingsResponse': case 'RatingsResponse':
return RatingsResponse.fromJson(value); return RatingsResponse.fromJson(value);
case 'RatingsUpdate': case 'RatingsUpdate':
@@ -448,6 +447,8 @@ class ApiClient {
return ReactionTypeTypeTransformer().decode(value); return ReactionTypeTypeTransformer().decode(value);
case 'ReverseGeocodingStateResponseDto': case 'ReverseGeocodingStateResponseDto':
return ReverseGeocodingStateResponseDto.fromJson(value); return ReverseGeocodingStateResponseDto.fromJson(value);
case 'ScanLibraryDto':
return ScanLibraryDto.fromJson(value);
case 'SearchAlbumResponseDto': case 'SearchAlbumResponseDto':
return SearchAlbumResponseDto.fromJson(value); return SearchAlbumResponseDto.fromJson(value);
case 'SearchAssetResponseDto': case 'SearchAssetResponseDto':
@@ -572,8 +573,6 @@ class ApiClient {
return TagsResponse.fromJson(value); return TagsResponse.fromJson(value);
case 'TagsUpdate': case 'TagsUpdate':
return TagsUpdate.fromJson(value); return TagsUpdate.fromJson(value);
case 'TestEmailResponseDto':
return TestEmailResponseDto.fromJson(value);
case 'TimeBucketResponseDto': case 'TimeBucketResponseDto':
return TimeBucketResponseDto.fromJson(value); return TimeBucketResponseDto.fromJson(value);
case 'TimeBucketSize': case 'TimeBucketSize':
@@ -584,8 +583,6 @@ class ApiClient {
return TranscodeHWAccelTypeTransformer().decode(value); return TranscodeHWAccelTypeTransformer().decode(value);
case 'TranscodePolicy': case 'TranscodePolicy':
return TranscodePolicyTypeTransformer().decode(value); return TranscodePolicyTypeTransformer().decode(value);
case 'TrashResponseDto':
return TrashResponseDto.fromJson(value);
case 'UpdateAlbumDto': case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value); return UpdateAlbumDto.fromJson(value);
case 'UpdateAlbumUserDto': case 'UpdateAlbumUserDto':
+9 -2
View File
@@ -14,30 +14,36 @@ class CLIPConfig {
/// Returns a new [CLIPConfig] instance. /// Returns a new [CLIPConfig] instance.
CLIPConfig({ CLIPConfig({
required this.enabled, required this.enabled,
required this.loadTextualModelOnConnection,
required this.modelName, required this.modelName,
}); });
bool enabled; bool enabled;
LoadTextualModelOnConnection loadTextualModelOnConnection;
String modelName; String modelName;
@override @override
bool operator ==(Object other) => identical(this, other) || other is CLIPConfig && bool operator ==(Object other) => identical(this, other) || other is CLIPConfig &&
other.enabled == enabled && other.enabled == enabled &&
other.loadTextualModelOnConnection == loadTextualModelOnConnection &&
other.modelName == modelName; other.modelName == modelName;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(enabled.hashCode) + (enabled.hashCode) +
(loadTextualModelOnConnection.hashCode) +
(modelName.hashCode); (modelName.hashCode);
@override @override
String toString() => 'CLIPConfig[enabled=$enabled, modelName=$modelName]'; String toString() => 'CLIPConfig[enabled=$enabled, loadTextualModelOnConnection=$loadTextualModelOnConnection, modelName=$modelName]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'enabled'] = this.enabled; json[r'enabled'] = this.enabled;
json[r'loadTextualModelOnConnection'] = this.loadTextualModelOnConnection;
json[r'modelName'] = this.modelName; json[r'modelName'] = this.modelName;
return json; return json;
} }
@@ -46,12 +52,12 @@ class CLIPConfig {
/// [value] if it's a [Map], null otherwise. /// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static CLIPConfig? fromJson(dynamic value) { static CLIPConfig? fromJson(dynamic value) {
upgradeDto(value, "CLIPConfig");
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return CLIPConfig( return CLIPConfig(
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
loadTextualModelOnConnection: LoadTextualModelOnConnection.fromJson(json[r'loadTextualModelOnConnection'])!,
modelName: mapValueOfType<String>(json, r'modelName')!, modelName: mapValueOfType<String>(json, r'modelName')!,
); );
} }
@@ -101,6 +107,7 @@ class CLIPConfig {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'enabled', 'enabled',
'loadTextualModelOnConnection',
'modelName', 'modelName',
}; };
} }
@@ -0,0 +1,107 @@
//
// 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 LoadTextualModelOnConnection {
/// Returns a new [LoadTextualModelOnConnection] instance.
LoadTextualModelOnConnection({
required this.enabled,
required this.ttl,
});
bool enabled;
/// Minimum value: 0
num ttl;
@override
bool operator ==(Object other) => identical(this, other) || other is LoadTextualModelOnConnection &&
other.enabled == enabled &&
other.ttl == ttl;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode) +
(ttl.hashCode);
@override
String toString() => 'LoadTextualModelOnConnection[enabled=$enabled, ttl=$ttl]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
json[r'ttl'] = this.ttl;
return json;
}
/// Returns a new [LoadTextualModelOnConnection] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static LoadTextualModelOnConnection? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return LoadTextualModelOnConnection(
enabled: mapValueOfType<bool>(json, r'enabled')!,
ttl: num.parse('${json[r'ttl']}'),
);
}
return null;
}
static List<LoadTextualModelOnConnection> listFromJson(dynamic json, {bool growable = false,}) {
final result = <LoadTextualModelOnConnection>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = LoadTextualModelOnConnection.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, LoadTextualModelOnConnection> mapFromJson(dynamic json) {
final map = <String, LoadTextualModelOnConnection>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = LoadTextualModelOnConnection.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of LoadTextualModelOnConnection-objects as value to a dart map
static Map<String, List<LoadTextualModelOnConnection>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<LoadTextualModelOnConnection>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = LoadTextualModelOnConnection.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
'ttl',
};
}
+125
View File
@@ -0,0 +1,125 @@
//
// 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 ScanLibraryDto {
/// Returns a new [ScanLibraryDto] instance.
ScanLibraryDto({
this.refreshAllFiles,
this.refreshModifiedFiles,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? refreshAllFiles;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? refreshModifiedFiles;
@override
bool operator ==(Object other) => identical(this, other) || other is ScanLibraryDto &&
other.refreshAllFiles == refreshAllFiles &&
other.refreshModifiedFiles == refreshModifiedFiles;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(refreshAllFiles == null ? 0 : refreshAllFiles!.hashCode) +
(refreshModifiedFiles == null ? 0 : refreshModifiedFiles!.hashCode);
@override
String toString() => 'ScanLibraryDto[refreshAllFiles=$refreshAllFiles, refreshModifiedFiles=$refreshModifiedFiles]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.refreshAllFiles != null) {
json[r'refreshAllFiles'] = this.refreshAllFiles;
} else {
// json[r'refreshAllFiles'] = null;
}
if (this.refreshModifiedFiles != null) {
json[r'refreshModifiedFiles'] = this.refreshModifiedFiles;
} else {
// json[r'refreshModifiedFiles'] = null;
}
return json;
}
/// Returns a new [ScanLibraryDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ScanLibraryDto? fromJson(dynamic value) {
upgradeDto(value, "ScanLibraryDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return ScanLibraryDto(
refreshAllFiles: mapValueOfType<bool>(json, r'refreshAllFiles'),
refreshModifiedFiles: mapValueOfType<bool>(json, r'refreshModifiedFiles'),
);
}
return null;
}
static List<ScanLibraryDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ScanLibraryDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ScanLibraryDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ScanLibraryDto> mapFromJson(dynamic json) {
final map = <String, ScanLibraryDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ScanLibraryDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ScanLibraryDto-objects as value to a dart map
static Map<String, List<ScanLibraryDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ScanLibraryDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ScanLibraryDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
-99
View File
@@ -1,99 +0,0 @@
//
// 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 TestEmailResponseDto {
/// Returns a new [TestEmailResponseDto] instance.
TestEmailResponseDto({
required this.messageId,
});
String messageId;
@override
bool operator ==(Object other) => identical(this, other) || other is TestEmailResponseDto &&
other.messageId == messageId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(messageId.hashCode);
@override
String toString() => 'TestEmailResponseDto[messageId=$messageId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'messageId'] = this.messageId;
return json;
}
/// Returns a new [TestEmailResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static TestEmailResponseDto? fromJson(dynamic value) {
upgradeDto(value, "TestEmailResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return TestEmailResponseDto(
messageId: mapValueOfType<String>(json, r'messageId')!,
);
}
return null;
}
static List<TestEmailResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <TestEmailResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = TestEmailResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, TestEmailResponseDto> mapFromJson(dynamic json) {
final map = <String, TestEmailResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = TestEmailResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of TestEmailResponseDto-objects as value to a dart map
static Map<String, List<TestEmailResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<TestEmailResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = TestEmailResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'messageId',
};
}
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.116.0+160 version: 1.115.0+159
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
+76 -20
View File
@@ -2853,6 +2853,41 @@
] ]
} }
}, },
"/libraries/{id}/removeOffline": {
"post": {
"operationId": "removeOfflineFiles",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Libraries"
]
}
},
"/libraries/{id}/scan": { "/libraries/{id}/scan": {
"post": { "post": {
"operationId": "scanLibrary", "operationId": "scanLibrary",
@@ -2867,6 +2902,16 @@
} }
} }
], ],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScanLibraryDto"
}
}
},
"required": true
},
"responses": { "responses": {
"204": { "204": {
"description": "" "description": ""
@@ -3446,13 +3491,6 @@
}, },
"responses": { "responses": {
"200": { "200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestEmailResponseDto"
}
}
},
"description": "" "description": ""
} }
}, },
@@ -5269,8 +5307,8 @@
"name": "password", "name": "password",
"required": false, "required": false,
"in": "query", "in": "query",
"example": "password",
"schema": { "schema": {
"example": "password",
"type": "string" "type": "string"
} }
}, },
@@ -7409,7 +7447,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.116.0", "version": "1.115.0",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],
@@ -8242,6 +8280,9 @@
"isFavorite": { "isFavorite": {
"type": "boolean" "type": "boolean"
}, },
"isOffline": {
"type": "boolean"
},
"isVisible": { "isVisible": {
"type": "boolean" "type": "boolean"
}, },
@@ -8615,12 +8656,16 @@
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
}, },
"loadTextualModelOnConnection": {
"$ref": "#/components/schemas/LoadTextualModelOnConnection"
},
"modelName": { "modelName": {
"type": "string" "type": "string"
} }
}, },
"required": [ "required": [
"enabled", "enabled",
"loadTextualModelOnConnection",
"modelName" "modelName"
], ],
"type": "object" "type": "object"
@@ -9461,6 +9506,17 @@
], ],
"type": "object" "type": "object"
}, },
"LoadTextualModelOnConnection": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"LogLevel": { "LogLevel": {
"enum": [ "enum": [
"verbose", "verbose",
@@ -10580,6 +10636,17 @@
], ],
"type": "object" "type": "object"
}, },
"ScanLibraryDto": {
"properties": {
"refreshAllFiles": {
"type": "boolean"
},
"refreshModifiedFiles": {
"type": "boolean"
}
},
"type": "object"
},
"SearchAlbumResponseDto": { "SearchAlbumResponseDto": {
"properties": { "properties": {
"count": { "count": {
@@ -12296,17 +12363,6 @@
}, },
"type": "object" "type": "object"
}, },
"TestEmailResponseDto": {
"properties": {
"messageId": {
"type": "string"
}
},
"required": [
"messageId"
],
"type": "object"
},
"TimeBucketResponseDto": { "TimeBucketResponseDto": {
"properties": { "properties": {
"count": { "count": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.115.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.115.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.115.0",
"description": "Auto-generated TypeScript SDK for the Immich API", "description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module", "type": "module",
"main": "./build/index.js", "main": "./build/index.js",
+24 -10
View File
@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.116.0 * 1.115.0
* DO NOT MODIFY - This file has been generated using oazapfts. * DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts * See https://www.npmjs.com/package/oazapfts
*/ */
@@ -366,6 +366,7 @@ export type AssetMediaCreateDto = {
fileModifiedAt: string; fileModifiedAt: string;
isArchived?: boolean; isArchived?: boolean;
isFavorite?: boolean; isFavorite?: boolean;
isOffline?: boolean;
isVisible?: boolean; isVisible?: boolean;
livePhotoVideoId?: string; livePhotoVideoId?: string;
sidecarData?: Blob; sidecarData?: Blob;
@@ -578,6 +579,10 @@ export type UpdateLibraryDto = {
importPaths?: string[]; importPaths?: string[];
name?: string; name?: string;
}; };
export type ScanLibraryDto = {
refreshAllFiles?: boolean;
refreshModifiedFiles?: boolean;
};
export type LibraryStatsResponseDto = { export type LibraryStatsResponseDto = {
photos: number; photos: number;
total: number; total: number;
@@ -651,9 +656,6 @@ export type SystemConfigSmtpDto = {
replyTo: string; replyTo: string;
transport: SystemConfigSmtpTransportDto; transport: SystemConfigSmtpTransportDto;
}; };
export type TestEmailResponseDto = {
messageId: string;
};
export type OAuthConfigDto = { export type OAuthConfigDto = {
redirectUri: string; redirectUri: string;
}; };
@@ -1140,8 +1142,13 @@ export type SystemConfigLoggingDto = {
enabled: boolean; enabled: boolean;
level: LogLevel; level: LogLevel;
}; };
export type LoadTextualModelOnConnection = {
enabled: boolean;
ttl: number;
};
export type ClipConfig = { export type ClipConfig = {
enabled: boolean; enabled: boolean;
loadTextualModelOnConnection: LoadTextualModelOnConnection;
modelName: string; modelName: string;
}; };
export type DuplicateDetectionConfig = { export type DuplicateDetectionConfig = {
@@ -2061,14 +2068,24 @@ export function updateLibrary({ id, updateLibraryDto }: {
body: updateLibraryDto body: updateLibraryDto
}))); })));
} }
export function scanLibrary({ id }: { export function removeOfflineFiles({ id }: {
id: string; id: string;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/scan`, { return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/removeOffline`, {
...opts, ...opts,
method: "POST" method: "POST"
})); }));
} }
export function scanLibrary({ id, scanLibraryDto }: {
id: string;
scanLibraryDto: ScanLibraryDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/scan`, oazapfts.json({
...opts,
method: "POST",
body: scanLibraryDto
})));
}
export function getLibraryStatistics({ id }: { export function getLibraryStatistics({ id }: {
id: string; id: string;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
@@ -2208,10 +2225,7 @@ export function addMemoryAssets({ id, bulkIdsDto }: {
export function sendTestEmail({ systemConfigSmtpDto }: { export function sendTestEmail({ systemConfigSmtpDto }: {
systemConfigSmtpDto: SystemConfigSmtpDto; systemConfigSmtpDto: SystemConfigSmtpDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchText("/notifications/test-email", oazapfts.json({
status: 200;
data: TestEmailResponseDto;
}>("/notifications/test-email", oazapfts.json({
...opts, ...opts,
method: "POST", method: "POST",
body: systemConfigSmtpDto body: systemConfigSmtpDto
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich", "name": "immich",
"version": "1.116.0", "version": "1.115.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich", "name": "immich",
"version": "1.116.0", "version": "1.115.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@nestjs/bullmq": "^10.0.1", "@nestjs/bullmq": "^10.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.116.0", "version": "1.115.0",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
+6
View File
@@ -120,6 +120,9 @@ export interface SystemConfig {
clip: { clip: {
enabled: boolean; enabled: boolean;
modelName: string; modelName: string;
loadTextualModelOnConnection: {
enabled: boolean;
};
}; };
duplicateDetection: { duplicateDetection: {
enabled: boolean; enabled: boolean;
@@ -270,6 +273,9 @@ export const defaults = Object.freeze<SystemConfig>({
clip: { clip: {
enabled: true, enabled: true,
modelName: 'ViT-B-32__openai', modelName: 'ViT-B-32__openai',
loadTextualModelOnConnection: {
enabled: false,
},
}, },
duplicateDetection: { duplicateDetection: {
enabled: true, enabled: true,
+18 -10
View File
@@ -4,6 +4,7 @@ import {
CreateLibraryDto, CreateLibraryDto,
LibraryResponseDto, LibraryResponseDto,
LibraryStatsResponseDto, LibraryStatsResponseDto,
ScanLibraryDto,
UpdateLibraryDto, UpdateLibraryDto,
ValidateLibraryDto, ValidateLibraryDto,
ValidateLibraryResponseDto, ValidateLibraryResponseDto,
@@ -42,13 +43,6 @@ export class LibraryController {
return this.service.update(id, dto); return this.service.update(id, dto);
} }
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true })
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(id);
}
@Post(':id/validate') @Post(':id/validate')
@HttpCode(200) @HttpCode(200)
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@@ -57,6 +51,13 @@ export class LibraryController {
return this.service.validate(id, dto); return this.service.validate(id, dto);
} }
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true })
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(id);
}
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true })
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> { getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
@@ -65,8 +66,15 @@ export class LibraryController {
@Post(':id/scan') @Post(':id/scan')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) @Authenticated({ admin: true })
scanLibrary(@Param() { id }: UUIDParamDto) { scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) {
return this.service.queueScan(id); return this.service.queueScan(id, dto);
}
@Post(':id/removeOffline')
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ admin: true })
removeOfflineFiles(@Param() { id }: UUIDParamDto) {
return this.service.queueRemoveOffline(id);
} }
} }
@@ -1,7 +1,6 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TestEmailResponseDto } from 'src/dtos/notification.dto';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { NotificationService } from 'src/services/notification.service'; import { NotificationService } from 'src/services/notification.service';
@@ -14,7 +13,7 @@ export class NotificationController {
@Post('test-email') @Post('test-email')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Authenticated({ admin: true }) @Authenticated({ admin: true })
sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise<TestEmailResponseDto> { sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto) {
return this.service.sendTestEmail(auth.user.id, dto); return this.service.sendTestEmail(auth.user.id, dto);
} }
} }
+3
View File
@@ -56,6 +56,9 @@ export class AssetMediaCreateDto extends AssetMediaBase {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isVisible?: boolean; isVisible?: boolean;
@ValidateBoolean({ optional: true })
isOffline?: boolean;
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
livePhotoVideoId?: string; livePhotoVideoId?: string;
+9 -1
View File
@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator'; import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
import { LibraryEntity } from 'src/entities/library.entity'; import { LibraryEntity } from 'src/entities/library.entity';
import { Optional, ValidateUUID } from 'src/validation'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class CreateLibraryDto { export class CreateLibraryDto {
@ValidateUUID() @ValidateUUID()
@@ -89,6 +89,14 @@ export class LibrarySearchDto {
userId?: string; userId?: string;
} }
export class ScanLibraryDto {
@ValidateBoolean({ optional: true })
refreshModifiedFiles?: boolean;
@ValidateBoolean({ optional: true })
refreshAllFiles?: boolean;
}
export class LibraryResponseDto { export class LibraryResponseDto {
id!: string; id!: string;
ownerId!: string; ownerId!: string;
+12 -2
View File
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; import { IsNotEmpty, IsNumber, IsObject, IsString, Max, Min, ValidateNested } from 'class-validator';
import { ValidateBoolean } from 'src/validation'; import { ValidateBoolean } from 'src/validation';
export class TaskConfig { export class TaskConfig {
@@ -14,7 +14,17 @@ export class ModelConfig extends TaskConfig {
modelName!: string; modelName!: string;
} }
export class CLIPConfig extends ModelConfig {} export class LoadTextualModelOnConnection {
@ValidateBoolean()
enabled!: boolean;
}
export class CLIPConfig extends ModelConfig {
@Type(() => LoadTextualModelOnConnection)
@ValidateNested()
@IsObject()
loadTextualModelOnConnection!: LoadTextualModelOnConnection;
}
export class DuplicateDetectionConfig extends TaskConfig { export class DuplicateDetectionConfig extends TaskConfig {
@IsNumber() @IsNumber()
-3
View File
@@ -1,3 +0,0 @@
export class TestEmailResponseDto {
messageId!: string;
}
+3
View File
@@ -36,6 +36,8 @@ export enum WithoutProperty {
export enum WithProperty { export enum WithProperty {
SIDECAR = 'sidecar', SIDECAR = 'sidecar',
IS_ONLINE = 'isOnline',
IS_OFFLINE = 'isOffline',
} }
export enum TimeBucketSize { export enum TimeBucketSize {
@@ -174,6 +176,7 @@ export interface IAssetRepository {
): Paginated<AssetEntity>; ): Paginated<AssetEntity>;
getRandom(userIds: string[], count: number): Promise<AssetEntity[]>; getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>; getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity>;
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>; getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>;
deleteAll(ownerId: string): Promise<void>; deleteAll(ownerId: string): Promise<void>;
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
+16 -11
View File
@@ -76,12 +76,12 @@ export enum JobName {
FACIAL_RECOGNITION = 'facial-recognition', FACIAL_RECOGNITION = 'facial-recognition',
// library management // library management
LIBRARY_QUEUE_SYNC_FILES = 'library-queue-sync-files', LIBRARY_SCAN = 'library-refresh',
LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-sync-assets', LIBRARY_SCAN_ASSET = 'library-refresh-asset',
LIBRARY_SYNC_FILE = 'library-sync-file', LIBRARY_REMOVE_OFFLINE = 'library-remove-offline',
LIBRARY_SYNC_ASSET = 'library-sync-asset', LIBRARY_CHECK_OFFLINE = 'library-check-offline',
LIBRARY_DELETE = 'library-delete', LIBRARY_DELETE = 'library-delete',
LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all', LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh',
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
// cleanup // cleanup
@@ -137,11 +137,16 @@ export interface ILibraryFileJob extends IEntityJob {
assetPath: string; assetPath: string;
} }
export interface ILibraryAssetJob extends IEntityJob { export interface ILibraryOfflineJob extends IEntityJob {
importPaths: string[]; importPaths: string[];
exclusionPatterns: string[]; exclusionPatterns: string[];
} }
export interface ILibraryRefreshJob extends IEntityJob {
refreshModifiedFiles: boolean;
refreshAllFiles: boolean;
}
export interface IBulkEntityJob extends IBaseJob { export interface IBulkEntityJob extends IBaseJob {
ids: string[]; ids: string[];
} }
@@ -272,12 +277,12 @@ export type JobItem =
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob } | { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
// Library Management // Library Management
| { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob } | { name: JobName.LIBRARY_SCAN_ASSET; data: ILibraryFileJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob } | { name: JobName.LIBRARY_SCAN; data: ILibraryRefreshJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob } | { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob }
| { name: JobName.LIBRARY_SYNC_ASSET; data: IEntityJob }
| { name: JobName.LIBRARY_DELETE; data: IEntityJob } | { name: JobName.LIBRARY_DELETE; data: IEntityJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob } | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob }
| { name: JobName.LIBRARY_CHECK_OFFLINE; data: IEntityJob }
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob } | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
// Notification // Notification
@@ -46,6 +46,11 @@ export interface Face {
score: number; score: number;
} }
export enum LoadTextModelActions {
LOAD,
UNLOAD,
}
export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse; export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse;
export type DetectedFaces = { faces: Face[] } & VisualResponse; export type DetectedFaces = { faces: Face[] } & VisualResponse;
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest; export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
@@ -54,4 +59,5 @@ export interface IMachineLearningRepository {
encodeImage(url: string, imagePath: string, config: ModelOptions): Promise<number[]>; encodeImage(url: string, imagePath: string, config: ModelOptions): Promise<number[]>;
encodeText(url: string, text: string, config: ModelOptions): Promise<number[]>; encodeText(url: string, text: string, config: ModelOptions): Promise<number[]>;
detectFaces(url: string, imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>; detectFaces(url: string, imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
prepareTextModel(url: string, config: ModelOptions, action: LoadTextModelActions): Promise<void>;
} }
+41
View File
@@ -268,6 +268,35 @@ DELETE FROM "assets"
WHERE WHERE
"ownerId" = $1 "ownerId" = $1
-- AssetRepository.getExternalLibraryAssetPaths
SELECT DISTINCT
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
FROM
(
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline"
FROM
"assets" "AssetEntity"
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
AND (
"AssetEntity__AssetEntity_library"."deletedAt" IS NULL
)
WHERE
(
(
((("AssetEntity__AssetEntity_library"."id" = $1)))
AND ("AssetEntity"."isExternal" = $2)
)
)
AND ("AssetEntity"."deletedAt" IS NULL)
) "distinctAlias"
ORDER BY
"AssetEntity_id" ASC
LIMIT
2
-- AssetRepository.getByLibraryIdAndOriginalPath -- AssetRepository.getByLibraryIdAndOriginalPath
SELECT DISTINCT SELECT DISTINCT
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
@@ -337,6 +366,18 @@ WHERE
AND "originalPath" = path AND "originalPath" = path
); );
-- AssetRepository.updateOfflineLibraryAssets
UPDATE "assets"
SET
"isOffline" = $1,
"updatedAt" = CURRENT_TIMESTAMP
WHERE
(
"libraryId" = $2
AND NOT ("originalPath" IN ($3))
AND "isOffline" = $4
)
-- AssetRepository.getAllByDeviceId -- AssetRepository.getAllByDeviceId
SELECT SELECT
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
+43 -17
View File
@@ -13,6 +13,7 @@ import {
AssetDeltaSyncOptions, AssetDeltaSyncOptions,
AssetExploreFieldOptions, AssetExploreFieldOptions,
AssetFullSyncOptions, AssetFullSyncOptions,
AssetPathEntity,
AssetStats, AssetStats,
AssetStatsOptions, AssetStatsOptions,
AssetUpdateAllOptions, AssetUpdateAllOptions,
@@ -176,6 +177,14 @@ export class AssetRepository implements IAssetRepository {
return this.getAll(pagination, { ...options, userIds: [userId] }); return this.getAll(pagination, { ...options, userIds: [userId] });
} }
@GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity> {
return paginate(this.repository, pagination, {
select: { id: true, originalPath: true, isOffline: true },
where: { library: { id: libraryId }, isExternal: true },
});
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null> { getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null> {
return this.repository.findOne({ return this.repository.findOne({
@@ -189,16 +198,24 @@ export class AssetRepository implements IAssetRepository {
async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise<string[]> { async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise<string[]> {
const result = await this.repository.query( const result = await this.repository.query(
` `
WITH paths AS (SELECT unnest($2::text[]) AS path) WITH paths AS (SELECT unnest($2::text[]) AS path)
SELECT path SELECT path FROM paths
FROM paths WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path);
WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path); `,
`,
[libraryId, originalPaths], [libraryId, originalPaths],
); );
return result.map((row: { path: string }) => row.path); return result.map((row: { path: string }) => row.path);
} }
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] })
@ChunkedArray({ paramIndex: 1 })
async updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise<void> {
await this.repository.update(
{ library: { id: libraryId }, originalPath: Not(In(originalPaths)), isOffline: false },
{ isOffline: true },
);
}
getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> { getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated<AssetEntity> {
let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files'); let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files');
builder = searchAssetBuilder(builder, options); builder = searchAssetBuilder(builder, options);
@@ -356,10 +373,12 @@ export class AssetRepository implements IAssetRepository {
} }
@GenerateSql( @GenerateSql(
...Object.values(WithProperty).map((property) => ({ ...Object.values(WithProperty)
name: property, .filter((property) => property !== WithProperty.IS_OFFLINE && property !== WithProperty.IS_ONLINE)
params: [DummyValue.PAGINATION, property], .map((property) => ({
})), name: property,
params: [DummyValue.PAGINATION, property],
})),
) )
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> { getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity> {
let relations: FindOptionsRelations<AssetEntity> = {}; let relations: FindOptionsRelations<AssetEntity> = {};
@@ -512,16 +531,26 @@ export class AssetRepository implements IAssetRepository {
where = [{ sidecarPath: Not(IsNull()), isVisible: true }]; where = [{ sidecarPath: Not(IsNull()), isVisible: true }];
break; break;
} }
case WithProperty.IS_OFFLINE: {
if (!libraryId) {
throw new Error('Library id is required when finding offline assets');
}
where = [{ isOffline: true, libraryId }];
break;
}
case WithProperty.IS_ONLINE: {
if (!libraryId) {
throw new Error('Library id is required when finding online assets');
}
where = [{ isOffline: false, libraryId }];
break;
}
default: { default: {
throw new Error(`Invalid getWith property: ${property}`); throw new Error(`Invalid getWith property: ${property}`);
} }
} }
if (libraryId) {
where = [{ ...where, libraryId }];
}
return paginate(this.repository, pagination, { return paginate(this.repository, pagination, {
where, where,
withDeleted, withDeleted,
@@ -721,10 +750,7 @@ export class AssetRepository implements IAssetRepository {
builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted(); builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted();
if (options.isTrashed) { if (options.isTrashed) {
// TODO: Temporarily inverted to support showing offline assets in the trash queries. builder.andWhere('asset.status = :status', { status: AssetStatus.TRASHED });
// Once offline assets are handled in a separate screen, this should be set back to status = TRASHED
// and the offline screens should use a separate isOffline = true parameter in the timeline query.
builder.andWhere('asset.status != :status', { status: AssetStatus.DELETED });
} }
} }
@@ -9,6 +9,7 @@ import {
WebSocketServer, WebSocketServer,
} from '@nestjs/websockets'; } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io'; import { Server, Socket } from 'socket.io';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { import {
ArgsOf, ArgsOf,
ClientEventMap, ClientEventMap,
@@ -19,6 +20,8 @@ import {
ServerEventMap, ServerEventMap,
} from 'src/interfaces/event.interface'; } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository, LoadTextModelActions } from 'src/interfaces/machine-learning.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { AuthService } from 'src/services/auth.service'; import { AuthService } from 'src/services/auth.service';
import { Instrumentation } from 'src/utils/instrumentation'; import { Instrumentation } from 'src/utils/instrumentation';
@@ -33,6 +36,7 @@ type EmitHandlers = Partial<{ [T in EmitEvent]: EmitHandler<T>[] }>;
@Injectable() @Injectable()
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository { export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository {
private emitHandlers: EmitHandlers = {}; private emitHandlers: EmitHandlers = {};
private configCore: SystemConfigCore;
@WebSocketServer() @WebSocketServer()
private server?: Server; private server?: Server;
@@ -41,8 +45,11 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
private eventEmitter: EventEmitter2, private eventEmitter: EventEmitter2,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
) { ) {
this.logger.setContext(EventRepository.name); this.logger.setContext(EventRepository.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
} }
afterInit(server: Server) { afterInit(server: Server) {
@@ -68,6 +75,21 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
queryParams: {}, queryParams: {},
metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' }, metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' },
}); });
if ('background' in client.handshake.query && client.handshake.query.background === 'false') {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
if (machineLearning.clip.loadTextualModelOnConnection.enabled) {
try {
console.log(this.server);
this.machineLearningRepository.prepareTextModel(
machineLearning.url,
machineLearning.clip,
LoadTextModelActions.LOAD,
);
} catch (error) {
this.logger.warn(error);
}
}
}
await client.join(auth.user.id); await client.join(auth.user.id);
if (auth.session) { if (auth.session) {
await client.join(auth.session.id); await client.join(auth.session.id);
@@ -83,6 +105,21 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
async handleDisconnect(client: Socket) { async handleDisconnect(client: Socket) {
this.logger.log(`Websocket Disconnect: ${client.id}`); this.logger.log(`Websocket Disconnect: ${client.id}`);
await client.leave(client.nsp.name); await client.leave(client.nsp.name);
if ('background' in client.handshake.query && client.handshake.query.background === 'false') {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
if (machineLearning.clip.loadTextualModelOnConnection.enabled && this.server?.engine.clientsCount == 0) {
try {
this.machineLearningRepository.prepareTextModel(
machineLearning.url,
machineLearning.clip,
LoadTextModelActions.UNLOAD,
);
this.logger.debug('sent request to unload text model');
} catch (error) {
this.logger.warn(error);
}
}
}
} }
on<T extends EmitEvent>(event: T, handler: EmitHandler<T>): void { on<T extends EmitEvent>(event: T, handler: EmitHandler<T>): void {
+5 -5
View File
@@ -79,12 +79,12 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.SIDECAR_WRITE]: QueueName.SIDECAR, [JobName.SIDECAR_WRITE]: QueueName.SIDECAR,
// Library management // Library management
[JobName.LIBRARY_SYNC_FILE]: QueueName.LIBRARY, [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_FILES]: QueueName.LIBRARY, [JobName.LIBRARY_SCAN]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_ASSETS]: QueueName.LIBRARY,
[JobName.LIBRARY_DELETE]: QueueName.LIBRARY, [JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
[JobName.LIBRARY_SYNC_ASSET]: QueueName.LIBRARY, [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_ALL]: QueueName.LIBRARY, [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
// Notification // Notification
@@ -7,6 +7,7 @@ import {
FaceDetectionOptions, FaceDetectionOptions,
FacialRecognitionResponse, FacialRecognitionResponse,
IMachineLearningRepository, IMachineLearningRepository,
LoadTextModelActions,
MachineLearningRequest, MachineLearningRequest,
ModelPayload, ModelPayload,
ModelTask, ModelTask,
@@ -20,13 +21,9 @@ const errorPrefix = 'Machine learning request';
@Injectable() @Injectable()
export class MachineLearningRepository implements IMachineLearningRepository { export class MachineLearningRepository implements IMachineLearningRepository {
private async predict<T>(url: string, payload: ModelPayload, config: MachineLearningRequest): Promise<T> { private async predict<T>(url: string, payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
const formData = await this.getFormData(payload, config); const formData = await this.getFormData(config, payload);
const res = await fetch(new URL('/predict', url), { method: 'POST', body: formData }).catch( const res = await this.fetchData(url, '/predict', formData);
(error: Error | any) => {
throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`);
},
);
if (res.status >= 400) { if (res.status >= 400) {
throw new Error(`${errorPrefix} '${JSON.stringify(config)}' failed with status ${res.status}: ${res.statusText}`); throw new Error(`${errorPrefix} '${JSON.stringify(config)}' failed with status ${res.status}: ${res.statusText}`);
@@ -34,6 +31,30 @@ export class MachineLearningRepository implements IMachineLearningRepository {
return res.json(); return res.json();
} }
private async fetchData(url: string, path: string, formData?: FormData): Promise<Response> {
const res = await fetch(new URL(path, url), { method: 'POST', body: formData }).catch((error: Error | any) => {
throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`);
});
return res;
}
private prepareTextModelUrl: Record<LoadTextModelActions, string> = {
[LoadTextModelActions.LOAD]: '/load',
[LoadTextModelActions.UNLOAD]: '/unload',
};
async prepareTextModel(url: string, { modelName }: CLIPConfig, actions: LoadTextModelActions) {
try {
const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName } } };
const formData = await this.getFormData(request);
const res = await this.fetchData(url, this.prepareTextModelUrl[actions], formData);
if (res.status >= 400) {
throw new Error(`${errorPrefix} Loadings textual model failed with status ${res.status}: ${res.statusText}`);
}
} catch (error) {}
}
async detectFaces(url: string, imagePath: string, { modelName, minScore }: FaceDetectionOptions) { async detectFaces(url: string, imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
const request = { const request = {
[ModelTask.FACIAL_RECOGNITION]: { [ModelTask.FACIAL_RECOGNITION]: {
@@ -61,16 +82,17 @@ export class MachineLearningRepository implements IMachineLearningRepository {
return response[ModelTask.SEARCH]; return response[ModelTask.SEARCH];
} }
private async getFormData(payload: ModelPayload, config: MachineLearningRequest): Promise<FormData> { private async getFormData(config: MachineLearningRequest, payload?: ModelPayload): Promise<FormData> {
const formData = new FormData(); const formData = new FormData();
formData.append('entries', JSON.stringify(config)); formData.append('entries', JSON.stringify(config));
if (payload) {
if ('imagePath' in payload) { if ('imagePath' in payload) {
formData.append('image', new Blob([await readFile(payload.imagePath)])); formData.append('image', new Blob([await readFile(payload.imagePath)]));
} else if ('text' in payload) { } else if ('text' in payload) {
formData.append('text', payload.text); formData.append('text', payload.text);
} else { } else {
throw new Error('Invalid input'); throw new Error('Invalid input');
}
} }
return formData; return formData;
+4 -7
View File
@@ -3,7 +3,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus } from 'src/enum'; import { AssetStatus } from 'src/enum';
import { ITrashRepository } from 'src/interfaces/trash.interface'; import { ITrashRepository } from 'src/interfaces/trash.interface';
import { Paginated, paginatedBuilder, PaginationOptions } from 'src/utils/pagination'; import { Paginated, paginatedBuilder, PaginationOptions } from 'src/utils/pagination';
import { In, Repository } from 'typeorm'; import { In, IsNull, Not, Repository } from 'typeorm';
export class TrashRepository implements ITrashRepository { export class TrashRepository implements ITrashRepository {
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {} constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
@@ -26,7 +26,7 @@ export class TrashRepository implements ITrashRepository {
async restore(userId: string): Promise<number> { async restore(userId: string): Promise<number> {
const result = await this.assetRepository.update( const result = await this.assetRepository.update(
{ ownerId: userId, status: AssetStatus.TRASHED }, { ownerId: userId, deletedAt: Not(IsNull()) },
{ status: AssetStatus.ACTIVE, deletedAt: null }, { status: AssetStatus.ACTIVE, deletedAt: null },
); );
@@ -35,7 +35,7 @@ export class TrashRepository implements ITrashRepository {
async empty(userId: string): Promise<number> { async empty(userId: string): Promise<number> {
const result = await this.assetRepository.update( const result = await this.assetRepository.update(
{ ownerId: userId, status: AssetStatus.TRASHED }, { ownerId: userId, deletedAt: Not(IsNull()), status: AssetStatus.TRASHED },
{ status: AssetStatus.DELETED }, { status: AssetStatus.DELETED },
); );
@@ -43,10 +43,7 @@ export class TrashRepository implements ITrashRepository {
} }
async restoreAll(ids: string[]): Promise<number> { async restoreAll(ids: string[]): Promise<number> {
const result = await this.assetRepository.update( const result = await this.assetRepository.update({ id: In(ids) }, { status: AssetStatus.ACTIVE, deletedAt: null });
{ id: In(ids), status: AssetStatus.TRASHED },
{ status: AssetStatus.ACTIVE, deletedAt: null },
);
return result.affected ?? 0; return result.affected ?? 0;
} }
} }
@@ -427,6 +427,7 @@ export class AssetMediaService {
livePhotoVideoId: dto.livePhotoVideoId, livePhotoVideoId: dto.livePhotoVideoId,
originalFileName: file.originalName, originalFileName: file.originalName,
sidecarPath: sidecarFile?.originalPath, sidecarPath: sidecarFile?.originalPath,
isOffline: dto.isOffline ?? false,
}); });
if (sidecarFile) { if (sidecarFile) {
+1 -1
View File
@@ -164,7 +164,7 @@ export class JobService {
} }
case QueueName.LIBRARY: { case QueueName.LIBRARY: {
return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, data: { force } }); return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } });
} }
default: { default: {
+409 -152
View File
@@ -10,8 +10,9 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { import {
IJobRepository, IJobRepository,
ILibraryAssetJob,
ILibraryFileJob, ILibraryFileJob,
ILibraryOfflineJob,
ILibraryRefreshJob,
JobName, JobName,
JOBS_LIBRARY_PAGINATION_SIZE, JOBS_LIBRARY_PAGINATION_SIZE,
JobStatus, JobStatus,
@@ -36,10 +37,6 @@ import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/sto
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
async function* mockWalk() {
yield await Promise.resolve(['/data/user1/photo.jpg']);
}
describe(LibraryService.name, () => { describe(LibraryService.name, () => {
let sut: LibraryService; let sut: LibraryService;
@@ -94,7 +91,7 @@ describe(LibraryService.name, () => {
enabled: true, enabled: true,
cronExpression: '0 1 * * *', cronExpression: '0 1 * * *',
}, },
watch: { enabled: true }, watch: { enabled: false },
}, },
} as SystemConfig); } as SystemConfig);
@@ -166,29 +163,102 @@ describe(LibraryService.name, () => {
describe('handleQueueAssetRefresh', () => { describe('handleQueueAssetRefresh', () => {
it('should queue refresh of a new asset', async () => { it('should queue refresh of a new asset', async () => {
const mockLibraryJob: ILibraryRefreshJob = {
id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: false,
refreshAllFiles: false,
};
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
storageMock.walk.mockImplementation(mockWalk); // eslint-disable-next-line @typescript-eslint/require-await
storageMock.walk.mockImplementation(async function* generator() {
yield ['/data/user1/photo.jpg'];
});
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id }); await sut.handleQueueAssetRefresh(mockLibraryJob);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.LIBRARY_SYNC_FILE, name: JobName.LIBRARY_SCAN_ASSET,
data: { data: {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: libraryStub.externalLibrary1.owner.id, ownerId: libraryStub.externalLibrary1.owner.id,
assetPath: '/data/user1/photo.jpg', assetPath: '/data/user1/photo.jpg',
force: false,
},
},
]);
});
it('should queue offline check of existing online assets', async () => {
const mockLibraryJob: ILibraryRefreshJob = {
id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: false,
refreshAllFiles: false,
};
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
storageMock.walk.mockImplementation(async function* generator() {});
assetMock.getWith.mockResolvedValue({ items: [assetStub.external], hasNextPage: false });
await sut.handleQueueAssetRefresh(mockLibraryJob);
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_CHECK_OFFLINE,
data: {
id: assetStub.external.id,
importPaths: libraryStub.externalLibrary1.importPaths,
exclusionPatterns: [],
}, },
}, },
]); ]);
}); });
it("should fail when library can't be found", async () => { it("should fail when library can't be found", async () => {
const mockLibraryJob: ILibraryRefreshJob = {
id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: false,
refreshAllFiles: false,
};
libraryMock.get.mockResolvedValue(null); libraryMock.get.mockResolvedValue(null);
await expect(sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
});
it('should force queue new assets', async () => {
const mockLibraryJob: ILibraryRefreshJob = {
id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: false,
refreshAllFiles: true,
};
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
// eslint-disable-next-line @typescript-eslint/require-await
storageMock.walk.mockImplementation(async function* generator() {
yield ['/data/user1/photo.jpg'];
});
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueAssetRefresh(mockLibraryJob);
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_SCAN_ASSET,
data: {
id: libraryStub.externalLibrary1.id,
ownerId: libraryStub.externalLibrary1.owner.id,
assetPath: '/data/user1/photo.jpg',
force: true,
},
},
]);
}); });
it('should ignore import paths that do not exist', async () => { it('should ignore import paths that do not exist', async () => {
@@ -206,9 +276,16 @@ describe(LibraryService.name, () => {
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); const mockLibraryJob: ILibraryRefreshJob = {
id: libraryStub.externalLibraryWithImportPaths1.id,
refreshModifiedFiles: false,
refreshAllFiles: false,
};
await sut.handleQueueSyncFiles({ id: libraryStub.externalLibraryWithImportPaths1.id }); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueAssetRefresh(mockLibraryJob);
expect(storageMock.walk).toHaveBeenCalledWith({ expect(storageMock.walk).toHaveBeenCalledWith({
pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]], pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]],
@@ -219,36 +296,9 @@ describe(LibraryService.name, () => {
}); });
}); });
describe('handleQueueRemoveDeleted', () => { describe('handleOfflineCheck', () => {
it('should queue online check of existing assets', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
storageMock.walk.mockImplementation(async function* generator() {});
assetMock.getAll.mockResolvedValue({ items: [assetStub.external], hasNextPage: false });
await sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id });
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_SYNC_ASSET,
data: {
id: assetStub.external.id,
importPaths: libraryStub.externalLibrary1.importPaths,
exclusionPatterns: [],
},
},
]);
});
it("should fail when library can't be found", async () => {
libraryMock.get.mockResolvedValue(null);
await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED);
});
});
describe('handleSyncAsset', () => {
it('should skip missing assets', async () => { it('should skip missing assets', async () => {
const mockAssetJob: ILibraryAssetJob = { const mockAssetJob: ILibraryOfflineJob = {
id: assetStub.external.id, id: assetStub.external.id,
importPaths: ['/'], importPaths: ['/'],
exclusionPatterns: [], exclusionPatterns: [],
@@ -256,31 +306,41 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(null); assetMock.getById.mockResolvedValue(null);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED);
expect(assetMock.remove).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalled();
});
it('should do nothing with already-offline assets', async () => {
const mockAssetJob: ILibraryOfflineJob = {
id: assetStub.external.id,
importPaths: ['/'],
exclusionPatterns: [],
};
assetMock.getById.mockResolvedValue(assetStub.offline);
await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.update).not.toHaveBeenCalled();
}); });
it('should offline assets no longer on disk', async () => { it('should offline assets no longer on disk', async () => {
const mockAssetJob: ILibraryAssetJob = { const mockAssetJob: ILibraryOfflineJob = {
id: assetStub.external.id, id: assetStub.external.id,
importPaths: ['/'], importPaths: ['/'],
exclusionPatterns: [], exclusionPatterns: [],
}; };
assetMock.getById.mockResolvedValue(assetStub.external); assetMock.getById.mockResolvedValue(assetStub.external);
storageMock.stat.mockRejectedValue(new Error('ENOENT, no such file or directory'));
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], { expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
isOffline: true,
deletedAt: expect.any(Date),
});
}); });
it('should offline assets matching an exclusion pattern', async () => { it('should offline assets matching an exclusion pattern', async () => {
const mockAssetJob: ILibraryAssetJob = { const mockAssetJob: ILibraryOfflineJob = {
id: assetStub.external.id, id: assetStub.external.id,
importPaths: ['/'], importPaths: ['/'],
exclusionPatterns: ['**/user1/**'], exclusionPatterns: ['**/user1/**'],
@@ -288,15 +348,13 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(assetStub.external); assetMock.getById.mockResolvedValue(assetStub.external);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
isOffline: true, expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
deletedAt: expect.any(Date),
});
}); });
it('should set assets outside of import paths as offline', async () => { it('should set assets outside of import paths as offline', async () => {
const mockAssetJob: ILibraryAssetJob = { const mockAssetJob: ILibraryOfflineJob = {
id: assetStub.external.id, id: assetStub.external.id,
importPaths: ['/data/user2'], importPaths: ['/data/user2'],
exclusionPatterns: [], exclusionPatterns: [],
@@ -305,74 +363,28 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(assetStub.external); assetMock.getById.mockResolvedValue(assetStub.external);
storageMock.checkFileExists.mockResolvedValue(true); storageMock.checkFileExists.mockResolvedValue(true);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], { expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
isOffline: true,
deletedAt: expect.any(Date),
});
}); });
it('should do nothing with online assets', async () => { it('should do nothing with online assets', async () => {
const mockAssetJob: ILibraryAssetJob = { const mockAssetJob: ILibraryOfflineJob = {
id: assetStub.external.id, id: assetStub.external.id,
importPaths: ['/'], importPaths: ['/'],
exclusionPatterns: [], exclusionPatterns: [],
}; };
assetMock.getById.mockResolvedValue(assetStub.external); assetMock.getById.mockResolvedValue(assetStub.external);
storageMock.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); storageMock.checkFileExists.mockResolvedValue(true);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalled();
});
it('should un-trash an asset previously marked as offline', async () => {
const mockAssetJob: ILibraryAssetJob = {
id: assetStub.external.id,
importPaths: ['/'],
exclusionPatterns: [],
};
assetMock.getById.mockResolvedValue(assetStub.trashedOffline);
storageMock.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.trashedOffline.id], {
deletedAt: null,
fileCreatedAt: assetStub.trashedOffline.fileModifiedAt,
fileModifiedAt: assetStub.trashedOffline.fileModifiedAt,
isOffline: false,
originalFileName: 'path.jpg',
});
}); });
}); });
it('should update file when mtime has changed', async () => { describe('handleAssetRefresh', () => {
const mockAssetJob: ILibraryAssetJob = {
id: assetStub.external.id,
importPaths: ['/'],
exclusionPatterns: [],
};
const newMTime = new Date();
assetMock.getById.mockResolvedValue(assetStub.external);
storageMock.stat.mockResolvedValue({ mtime: newMTime } as Stats);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
fileModifiedAt: newMTime,
fileCreatedAt: newMTime,
isOffline: false,
originalFileName: 'photo.jpg',
deletedAt: null,
});
});
describe('handleSyncFile', () => {
let mockUser: UserEntity; let mockUser: UserEntity;
beforeEach(() => { beforeEach(() => {
@@ -385,18 +397,42 @@ describe(LibraryService.name, () => {
} as Stats); } as Stats);
}); });
it('should import a new asset', async () => { it('should reject an unknown file extension', async () => {
const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id,
assetPath: '/data/user1/file.xyz',
force: false,
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
});
it('should reject an unknown file type', async () => {
const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id,
assetPath: '/data/user1/file.xyz',
force: false,
};
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
});
it('should add a new image', async () => {
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id, ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg', assetPath: '/data/user1/photo.jpg',
force: false,
}; };
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([ expect(assetMock.create.mock.calls).toEqual([
[ [
@@ -431,19 +467,19 @@ describe(LibraryService.name, () => {
]); ]);
}); });
it('should import a new asset with sidecar', async () => { it('should add a new image with sidecar', async () => {
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id, ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg', assetPath: '/data/user1/photo.jpg',
force: false,
}; };
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image);
storageMock.checkFileExists.mockResolvedValue(true); storageMock.checkFileExists.mockResolvedValue(true);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([ expect(assetMock.create.mock.calls).toEqual([
[ [
@@ -478,18 +514,18 @@ describe(LibraryService.name, () => {
]); ]);
}); });
it('should import a new video', async () => { it('should add a new video', async () => {
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id, ownerId: mockUser.id,
assetPath: '/data/user1/video.mp4', assetPath: '/data/user1/video.mp4',
force: false,
}; };
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.video); assetMock.create.mockResolvedValue(assetStub.video);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([ expect(assetMock.create.mock.calls).toEqual([
[ [
@@ -532,27 +568,29 @@ describe(LibraryService.name, () => {
]); ]);
}); });
it('should not import an asset to a soft deleted library', async () => { it('should not add an image to a soft deleted library', async () => {
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id, ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg', assetPath: '/data/user1/photo.jpg',
force: false,
}; };
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image);
libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() }); libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() });
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
expect(assetMock.create.mock.calls).toEqual([]); expect(assetMock.create.mock.calls).toEqual([]);
}); });
it('should not refresh a file whose mtime matches existing asset', async () => { it('should not import an asset when mtime matches db asset', async () => {
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id, ownerId: mockUser.id,
assetPath: assetStub.hasFileExtension.originalPath, assetPath: assetStub.hasFileExtension.originalPath,
force: false,
}; };
storageMock.stat.mockResolvedValue({ storageMock.stat.mockResolvedValue({
@@ -563,52 +601,190 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled();
expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled();
}); });
it('should skip existing asset', async () => { it('should import an asset when mtime differs from db asset', async () => {
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id, ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg', assetPath: '/data/user1/photo.jpg',
force: false,
}; };
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.METADATA_EXTRACTION,
data: {
id: assetStub.image.id,
source: 'upload',
},
});
expect(jobMock.queue).not.toHaveBeenCalledWith({
name: JobName.VIDEO_CONVERSION,
data: {
id: assetStub.image.id,
},
});
}); });
it('should not refresh an asset trashed by user', async () => { it('should import an asset that is missing a file extension', async () => {
// This tests for the case where the file extension is missing from the asset path.
// This happened in previous versions of Immich
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id, ownerId: mockUser.id,
assetPath: assetStub.hasFileExtension.originalPath, assetPath: assetStub.missingFileExtension.originalPath,
force: false,
}; };
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.trashed); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.missingFileExtension);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith(
[assetStub.missingFileExtension.id],
expect.objectContaining({ originalFileName: 'photo.jpg' }),
);
});
it('should set a missing asset to offline', async () => {
storageMock.stat.mockRejectedValue(new Error('Path not found'));
const mockLibraryJob: ILibraryFileJob = {
id: assetStub.image.id,
ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg',
force: false,
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true });
expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled();
expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled();
}); });
it('should throw BadRequestException when asset does not exist', async () => { it('should online a previously-offline asset', async () => {
const mockLibraryJob: ILibraryFileJob = {
id: assetStub.offline.id,
ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg',
force: false,
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.offline);
assetMock.create.mockResolvedValue(assetStub.offline);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false });
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.METADATA_EXTRACTION,
data: {
id: assetStub.offline.id,
source: 'upload',
},
});
expect(jobMock.queue).not.toHaveBeenCalledWith({
name: JobName.VIDEO_CONVERSION,
data: {
id: assetStub.offline.id,
},
});
});
it('should do nothing when mtime matches existing asset', async () => {
const mockLibraryJob: ILibraryFileJob = {
id: assetStub.image.id,
ownerId: assetStub.image.ownerId,
assetPath: '/data/user1/photo.jpg',
force: false,
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
assetMock.create.mockResolvedValue(assetStub.image);
expect(assetMock.update).not.toHaveBeenCalled();
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
});
it('should refresh an existing asset if forced', async () => {
const mockLibraryJob: ILibraryFileJob = {
id: assetStub.image.id,
ownerId: assetStub.hasFileExtension.ownerId,
assetPath: assetStub.hasFileExtension.originalPath,
force: true,
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
assetMock.create.mockResolvedValue(assetStub.hasFileExtension);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasFileExtension.id], {
fileCreatedAt: new Date('2023-01-01'),
fileModifiedAt: new Date('2023-01-01'),
originalFileName: assetStub.hasFileExtension.originalFileName,
});
});
it('should refresh an existing asset with modified mtime', async () => {
const filemtime = new Date();
filemtime.setSeconds(assetStub.image.fileModifiedAt.getSeconds() + 10);
const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id,
ownerId: userStub.admin.id,
assetPath: '/data/user1/photo.jpg',
force: false,
};
storageMock.stat.mockResolvedValue({
size: 100,
mtime: filemtime,
ctime: new Date('2023-01-01'),
} as Stats);
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create).toHaveBeenCalled();
const createdAsset = assetMock.create.mock.calls[0][0];
expect(createdAsset.fileModifiedAt).toEqual(filemtime);
});
it('should throw error when asset does not exist', async () => {
storageMock.stat.mockRejectedValue(new Error("ENOENT, no such file or directory '/data/user1/photo.jpg'")); storageMock.stat.mockRejectedValue(new Error("ENOENT, no such file or directory '/data/user1/photo.jpg'"));
const mockLibraryJob: ILibraryFileJob = { const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
ownerId: userStub.admin.id, ownerId: userStub.admin.id,
assetPath: '/data/user1/photo.jpg', assetPath: '/data/user1/photo.jpg',
force: false,
}; };
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
}); });
}); });
@@ -681,6 +857,7 @@ describe(LibraryService.name, () => {
describe('getStatistics', () => { describe('getStatistics', () => {
it('should return library statistics', async () => { it('should return library statistics', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 }); libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
await expect(sut.getStatistics(libraryStub.externalLibrary1.id)).resolves.toEqual({ await expect(sut.getStatistics(libraryStub.externalLibrary1.id)).resolves.toEqual({
photos: 10, photos: 10,
@@ -915,11 +1092,12 @@ describe(LibraryService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.LIBRARY_SYNC_FILE, name: JobName.LIBRARY_SCAN_ASSET,
data: { data: {
id: libraryStub.externalLibraryWithImportPaths1.id, id: libraryStub.externalLibraryWithImportPaths1.id,
assetPath: '/foo/photo.jpg', assetPath: '/foo/photo.jpg',
ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id, ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
force: false,
}, },
}, },
]); ]);
@@ -936,16 +1114,30 @@ describe(LibraryService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.LIBRARY_SYNC_FILE, name: JobName.LIBRARY_SCAN_ASSET,
data: { data: {
id: libraryStub.externalLibraryWithImportPaths1.id, id: libraryStub.externalLibraryWithImportPaths1.id,
assetPath: '/foo/photo.jpg', assetPath: '/foo/photo.jpg',
ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id, ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id,
force: false,
}, },
}, },
]); ]);
}); });
it('should handle a file unlink event', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
storageMock.watch.mockImplementation(
makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }),
);
await sut.watchAll();
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true });
});
it('should handle an error event', async () => { it('should handle an error event', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
@@ -1040,23 +1232,72 @@ describe(LibraryService.name, () => {
}); });
describe('queueScan', () => { describe('queueScan', () => {
it('should queue a library scan', async () => { it('should queue a library scan of external library', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await sut.queueScan(libraryStub.externalLibrary1.id); await sut.queueScan(libraryStub.externalLibrary1.id, {});
expect(jobMock.queue.mock.calls).toEqual([ expect(jobMock.queue.mock.calls).toEqual([
[ [
{ {
name: JobName.LIBRARY_QUEUE_SYNC_FILES, name: JobName.LIBRARY_SCAN,
data: { data: {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: false,
refreshAllFiles: false,
}, },
}, },
], ],
]);
});
it('should queue a library scan of all modified assets', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await sut.queueScan(libraryStub.externalLibrary1.id, { refreshModifiedFiles: true });
expect(jobMock.queue.mock.calls).toEqual([
[ [
{ {
name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, name: JobName.LIBRARY_SCAN,
data: {
id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: true,
refreshAllFiles: false,
},
},
],
]);
});
it('should queue a forced library scan', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await sut.queueScan(libraryStub.externalLibrary1.id, { refreshAllFiles: true });
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.LIBRARY_SCAN,
data: {
id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: false,
refreshAllFiles: true,
},
},
],
]);
});
});
describe('queueEmptyTrash', () => {
it('should queue the trash job', async () => {
await sut.queueRemoveOffline(libraryStub.externalLibrary1.id);
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.LIBRARY_REMOVE_OFFLINE,
data: { data: {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
}, },
@@ -1070,7 +1311,7 @@ describe(LibraryService.name, () => {
it('should queue the refresh job', async () => { it('should queue the refresh job', async () => {
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
await expect(sut.handleQueueSyncAll()).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleQueueAllScan({})).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queue.mock.calls).toEqual([ expect(jobMock.queue.mock.calls).toEqual([
[ [
@@ -1082,32 +1323,48 @@ describe(LibraryService.name, () => {
]); ]);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.LIBRARY_QUEUE_SYNC_FILES, name: JobName.LIBRARY_SCAN,
data: { data: {
id: libraryStub.externalLibrary1.id, id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: true,
refreshAllFiles: false,
},
},
]);
});
it('should queue the force refresh job', async () => {
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.LIBRARY_QUEUE_CLEANUP,
data: {},
});
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_SCAN,
data: {
id: libraryStub.externalLibrary1.id,
refreshModifiedFiles: false,
refreshAllFiles: true,
}, },
}, },
]); ]);
}); });
}); });
describe('handleQueueAssetOfflineCheck', () => { describe('handleRemoveOfflineFiles', () => {
it('should queue removal jobs', async () => { it('should queue trash deletion jobs', async () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
assetMock.getById.mockResolvedValue(assetStub.image1); assetMock.getById.mockResolvedValue(assetStub.image1);
await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleRemoveOffline({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ { name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id, deleteOnDisk: false } },
name: JobName.LIBRARY_SYNC_ASSET,
data: {
id: assetStub.image1.id,
importPaths: libraryStub.externalLibrary1.importPaths,
exclusionPatterns: libraryStub.externalLibrary1.exclusionPatterns,
},
},
]); ]);
}); });
}); });
+231 -172
View File
@@ -1,5 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { R_OK } from 'node:constants'; import { R_OK } from 'node:constants';
import { Stats } from 'node:fs';
import path, { basename, parse } from 'node:path'; import path, { basename, parse } from 'node:path';
import picomatch from 'picomatch'; import picomatch from 'picomatch';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
@@ -9,26 +10,27 @@ import {
CreateLibraryDto, CreateLibraryDto,
LibraryResponseDto, LibraryResponseDto,
LibraryStatsResponseDto, LibraryStatsResponseDto,
mapLibrary, ScanLibraryDto,
UpdateLibraryDto, UpdateLibraryDto,
ValidateLibraryDto, ValidateLibraryDto,
ValidateLibraryImportPathResponseDto, ValidateLibraryImportPathResponseDto,
ValidateLibraryResponseDto, ValidateLibraryResponseDto,
mapLibrary,
} from 'src/dtos/library.dto'; } from 'src/dtos/library.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { LibraryEntity } from 'src/entities/library.entity';
import { AssetType } from 'src/enum'; import { AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { import {
IBaseJob,
IEntityJob, IEntityJob,
IJobRepository, IJobRepository,
ILibraryAssetJob,
ILibraryFileJob, ILibraryFileJob,
JobName, ILibraryOfflineJob,
ILibraryRefreshJob,
JOBS_LIBRARY_PAGINATION_SIZE, JOBS_LIBRARY_PAGINATION_SIZE,
JobName,
JobStatus, JobStatus,
} from 'src/interfaces/job.interface'; } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface';
@@ -76,7 +78,11 @@ export class LibraryService {
this.jobRepository.addCronJob( this.jobRepository.addCronJob(
'libraryScan', 'libraryScan',
scan.cronExpression, scan.cronExpression,
() => handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger), () =>
handlePromiseError(
this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force: false } }),
this.logger,
),
scan.enabled, scan.enabled,
); );
@@ -137,7 +143,7 @@ export class LibraryService {
const handler = async () => { const handler = async () => {
this.logger.debug(`File add event received for ${path} in library ${library.id}}`); this.logger.debug(`File add event received for ${path} in library ${library.id}}`);
if (matcher(path)) { if (matcher(path)) {
await this.syncFiles(library, [path]); await this.scanAssets(library.id, [path], library.ownerId, false);
} }
}; };
return handlePromiseError(handler(), this.logger); return handlePromiseError(handler(), this.logger);
@@ -145,13 +151,9 @@ export class LibraryService {
onChange: (path) => { onChange: (path) => {
const handler = async () => { const handler = async () => {
this.logger.debug(`Detected file change for ${path} in library ${library.id}`); this.logger.debug(`Detected file change for ${path} in library ${library.id}`);
const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
if (asset) {
await this.syncAssets(library, [asset.id]);
}
if (matcher(path)) { if (matcher(path)) {
// Note: if the changed file was not previously imported, it will be imported now. // Note: if the changed file was not previously imported, it will be imported now.
await this.syncFiles(library, [path]); await this.scanAssets(library.id, [path], library.ownerId, false);
} }
}; };
return handlePromiseError(handler(), this.logger); return handlePromiseError(handler(), this.logger);
@@ -160,8 +162,8 @@ export class LibraryService {
const handler = async () => { const handler = async () => {
this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`);
const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path); const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
if (asset) { if (asset && matcher(path)) {
await this.syncAssets(library, [asset.id]); await this.assetRepository.update({ id: asset.id, isOffline: true });
} }
}; };
return handlePromiseError(handler(), this.logger); return handlePromiseError(handler(), this.logger);
@@ -214,7 +216,7 @@ export class LibraryService {
async getStatistics(id: string): Promise<LibraryStatsResponseDto> { async getStatistics(id: string): Promise<LibraryStatsResponseDto> {
const statistics = await this.repository.getStatistics(id); const statistics = await this.repository.getStatistics(id);
if (!statistics) { if (!statistics) {
throw new BadRequestException(`Library ${id} not found`); throw new BadRequestException('Library not found');
} }
return statistics; return statistics;
} }
@@ -248,28 +250,20 @@ export class LibraryService {
return mapLibrary(library); return mapLibrary(library);
} }
private async syncFiles({ id, ownerId }: LibraryEntity, assetPaths: string[]) { private async scanAssets(libraryId: string, assetPaths: string[], ownerId: string, force = false) {
await this.jobRepository.queueAll( await this.jobRepository.queueAll(
assetPaths.map((assetPath) => ({ assetPaths.map((assetPath) => ({
name: JobName.LIBRARY_SYNC_FILE, name: JobName.LIBRARY_SCAN_ASSET,
data: { data: {
id, id: libraryId,
assetPath, assetPath,
ownerId, ownerId,
force,
}, },
})), })),
); );
} }
private async syncAssets({ importPaths, exclusionPatterns }: LibraryEntity, assetIds: string[]) {
await this.jobRepository.queueAll(
assetIds.map((assetId) => ({
name: JobName.LIBRARY_SYNC_ASSET,
data: { id: assetId, importPaths, exclusionPatterns },
})),
);
}
private async validateImportPath(importPath: string): Promise<ValidateLibraryImportPathResponseDto> { private async validateImportPath(importPath: string): Promise<ValidateLibraryImportPathResponseDto> {
const validation = new ValidateLibraryImportPathResponseDto(); const validation = new ValidateLibraryImportPathResponseDto();
validation.importPath = importPath; validation.importPath = importPath;
@@ -372,182 +366,258 @@ export class LibraryService {
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleSyncFile(job: ILibraryFileJob): Promise<JobStatus> { async handleAssetRefresh(job: ILibraryFileJob): Promise<JobStatus> {
// Only needs to handle new assets
const assetPath = path.normalize(job.assetPath); const assetPath = path.normalize(job.assetPath);
let asset = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath); const existingAssetEntity = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath);
if (asset) {
let stats: Stats;
try {
stats = await this.storageRepository.stat(assetPath);
} catch (error: Error | any) {
// Can't access file, probably offline
if (existingAssetEntity) {
// Mark asset as offline
this.logger.debug(`Marking asset as offline: ${assetPath}`);
await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: true });
return JobStatus.SUCCESS;
} else {
// File can't be accessed and does not already exist in db
throw new BadRequestException('Cannot access file', { cause: error });
}
}
let doImport = false;
let doRefresh = false;
if (job.force) {
doRefresh = true;
}
const originalFileName = parse(assetPath).base;
if (!existingAssetEntity) {
// This asset is new to us, read it from disk
this.logger.debug(`Importing new asset: ${assetPath}`);
doImport = true;
} else if (stats.mtime.toISOString() !== existingAssetEntity.fileModifiedAt.toISOString()) {
// File modification time has changed since last time we checked, re-read from disk
this.logger.debug(
`File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`,
);
doRefresh = true;
} else if (existingAssetEntity.originalFileName !== originalFileName) {
// TODO: We can likely remove this check in the second half of 2024 when all assets have likely been re-imported by all users
this.logger.debug(
`Asset is missing file extension, re-importing: ${assetPath}. Current incorrect filename: ${existingAssetEntity.originalFileName}.`,
);
doRefresh = true;
} else if (!job.force && stats && !existingAssetEntity.isOffline) {
// Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing
this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`);
}
if (stats && existingAssetEntity?.isOffline) {
// File was previously offline but is now online
this.logger.debug(`Marking previously-offline asset as online: ${assetPath}`);
await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: false });
doRefresh = true;
}
if (!doImport && !doRefresh) {
// If we don't import, exit here
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
let stat; let assetType: AssetType;
try {
stat = await this.storageRepository.stat(assetPath); if (mimeTypes.isImage(assetPath)) {
} catch (error: any) { assetType = AssetType.IMAGE;
if (error.code === 'ENOENT') { } else if (mimeTypes.isVideo(assetPath)) {
this.logger.error(`File not found: ${assetPath}`); assetType = AssetType.VIDEO;
return JobStatus.SKIPPED; } else {
} throw new BadRequestException(`Unsupported file type ${assetPath}`);
this.logger.error(`Error reading file: ${assetPath}. Error: ${error}`);
return JobStatus.FAILED;
} }
this.logger.log(`Importing new library asset: ${assetPath}`);
const library = await this.repository.get(job.id, true);
if (!library || library.deletedAt) {
this.logger.error('Cannot import asset into deleted library');
return JobStatus.FAILED;
}
// TODO: device asset id is deprecated, remove it
const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, '');
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
// TODO: doesn't xmp replace the file extension? Will need investigation // TODO: doesn't xmp replace the file extension? Will need investigation
let sidecarPath: string | null = null; let sidecarPath: string | null = null;
if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) { if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) {
sidecarPath = `${assetPath}.xmp`; sidecarPath = `${assetPath}.xmp`;
} }
const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE; // TODO: device asset id is deprecated, remove it
const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, '');
const mtime = stat.mtime; let assetId;
if (doImport) {
const library = await this.repository.get(job.id, true);
if (library?.deletedAt) {
this.logger.error('Cannot import asset into deleted library');
return JobStatus.FAILED;
}
asset = await this.assetRepository.create({ const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
ownerId: job.ownerId,
libraryId: job.id,
checksum: pathHash,
originalPath: assetPath,
deviceAssetId,
deviceId: 'Library Import',
fileCreatedAt: mtime,
fileModifiedAt: mtime,
localDateTime: mtime,
type: assetType,
originalFileName: parse(assetPath).base,
sidecarPath, // TODO: In wait of refactoring the domain asset service, this function is just manually written like this
isExternal: true, const addedAsset = await this.assetRepository.create({
}); ownerId: job.ownerId,
libraryId: job.id,
checksum: pathHash,
originalPath: assetPath,
deviceAssetId,
deviceId: 'Library Import',
fileCreatedAt: stats.mtime,
fileModifiedAt: stats.mtime,
localDateTime: stats.mtime,
type: assetType,
originalFileName,
sidecarPath,
isExternal: true,
});
assetId = addedAsset.id;
} else if (doRefresh && existingAssetEntity) {
assetId = existingAssetEntity.id;
await this.assetRepository.updateAll([existingAssetEntity.id], {
fileCreatedAt: stats.mtime,
fileModifiedAt: stats.mtime,
originalFileName,
});
} else {
// Not importing and not refreshing, do nothing
return JobStatus.SKIPPED;
}
await this.queuePostSyncJobs(asset); this.logger.debug(`Queueing metadata extraction for: ${assetPath}`);
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: assetId, source: 'upload' } });
if (assetType === AssetType.VIDEO) {
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: assetId } });
}
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async queuePostSyncJobs(asset: AssetEntity) { async queueScan(id: string, dto: ScanLibraryDto) {
this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`);
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
if (asset.type === AssetType.VIDEO) {
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: asset.id } });
}
}
async queueScan(id: string) {
await this.findOrFail(id); await this.findOrFail(id);
await this.jobRepository.queue({ await this.jobRepository.queue({
name: JobName.LIBRARY_QUEUE_SYNC_FILES, name: JobName.LIBRARY_SCAN,
data: { data: {
id, id,
refreshModifiedFiles: dto.refreshModifiedFiles ?? false,
refreshAllFiles: dto.refreshAllFiles ?? false,
}, },
}); });
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
} }
async handleQueueSyncAll(): Promise<JobStatus> { async queueRemoveOffline(id: string) {
this.logger.debug(`Refreshing all external libraries`); this.logger.verbose(`Queueing offline file removal from library ${id}`);
await this.jobRepository.queue({ name: JobName.LIBRARY_REMOVE_OFFLINE, data: { id } });
}
async handleQueueAllScan(job: IBaseJob): Promise<JobStatus> {
this.logger.debug(`Refreshing all external libraries: force=${job.force}`);
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} });
const libraries = await this.repository.getAll(true); const libraries = await this.repository.getAll(true);
await this.jobRepository.queueAll( await this.jobRepository.queueAll(
libraries.map((library) => ({ libraries.map((library) => ({
name: JobName.LIBRARY_QUEUE_SYNC_FILES, name: JobName.LIBRARY_SCAN,
data: {
id: library.id,
},
})),
);
await this.jobRepository.queueAll(
libraries.map((library) => ({
name: JobName.LIBRARY_QUEUE_SYNC_ASSETS,
data: { data: {
id: library.id, id: library.id,
refreshModifiedFiles: !job.force,
refreshAllFiles: job.force ?? false,
}, },
})), })),
); );
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleSyncAsset(job: ILibraryAssetJob): Promise<JobStatus> { async handleOfflineCheck(job: ILibraryOfflineJob): Promise<JobStatus> {
const asset = await this.assetRepository.getById(job.id); const asset = await this.assetRepository.getById(job.id);
if (!asset) { if (!asset) {
// Asset is no longer in the database, skip
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
const markOffline = async (explanation: string) => { if (asset.isOffline) {
if (!asset.isOffline) { this.logger.verbose(`Asset is already offline: ${asset.originalPath}`);
this.logger.debug(`${explanation}, removing: ${asset.originalPath}`); return JobStatus.SUCCESS;
await this.assetRepository.updateAll([asset.id], { isOffline: true, deletedAt: new Date() }); }
}
};
const isInPath = job.importPaths.find((path) => asset.originalPath.startsWith(path)); const isInPath = job.importPaths.find((path) => asset.originalPath.startsWith(path));
if (!isInPath) { if (!isInPath) {
await markOffline('Asset is no longer in an import path'); this.logger.debug(`Asset is no longer in an import path, marking offline: ${asset.originalPath}`);
await this.assetRepository.update({ id: asset.id, isOffline: true });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern)); const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern));
if (isExcluded) { if (isExcluded) {
await markOffline('Asset is covered by an exclusion pattern'); this.logger.debug(`Asset is covered by an exclusion pattern, marking offline: ${asset.originalPath}`);
await this.assetRepository.update({ id: asset.id, isOffline: true });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
let stat; const fileExists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK);
try { if (!fileExists) {
stat = await this.storageRepository.stat(asset.originalPath); this.logger.debug(`Asset is no longer found on disk, marking offline: ${asset.originalPath}`);
} catch { await this.assetRepository.update({ id: asset.id, isOffline: true });
await markOffline('Asset is no longer on disk or is inaccessible because of permissions');
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
const mtime = stat.mtime; this.logger.verbose(
const isAssetModified = mtime.toISOString() !== asset.fileModifiedAt.toISOString(); `Asset is found on disk, not covered by an exclusion pattern, and is in an import path, keeping online: ${asset.originalPath}`,
);
if (asset.isOffline || isAssetModified) {
this.logger.debug(`Asset was offline or modified, updating asset record ${asset.originalPath}`);
//TODO: When we have asset status, we need to leave deletedAt as is when status is trashed
await this.assetRepository.updateAll([asset.id], {
isOffline: false,
deletedAt: null,
fileCreatedAt: mtime,
fileModifiedAt: mtime,
originalFileName: parse(asset.originalPath).base,
});
}
if (isAssetModified) {
this.logger.debug(`Asset was modified, queuing metadata extraction for: ${asset.originalPath}`);
await this.queuePostSyncJobs(asset);
}
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleQueueSyncFiles(job: IEntityJob): Promise<JobStatus> { async handleRemoveOffline(job: IEntityJob): Promise<JobStatus> {
this.logger.debug(`Removing offline assets for library ${job.id}`);
const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id, true),
);
let offlineAssets = 0;
for await (const assets of assetPagination) {
offlineAssets += assets.length;
if (assets.length > 0) {
this.logger.debug(`Discovered ${offlineAssets} offline assets in library ${job.id}`);
await this.jobRepository.queueAll(
assets.map((asset) => ({
name: JobName.ASSET_DELETION,
data: {
id: asset.id,
deleteOnDisk: false,
},
})),
);
this.logger.verbose(`Queued deletion of ${assets.length} offline assets in library ${job.id}`);
}
}
if (offlineAssets) {
this.logger.debug(`Finished queueing deletion of ${offlineAssets} offline assets for library ${job.id}`);
} else {
this.logger.debug(`Found no offline assets to delete from library ${job.id}`);
}
return JobStatus.SUCCESS;
}
async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise<JobStatus> {
const library = await this.repository.get(job.id); const library = await this.repository.get(job.id);
if (!library) { if (!library) {
this.logger.debug(`Library ${job.id} not found, skipping refresh`);
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
this.logger.log(`Refreshing library ${library.id} for new assets`); this.logger.log(`Refreshing library ${library.id}`);
const validImportPaths: string[] = []; const validImportPaths: string[] = [];
@@ -560,66 +630,55 @@ export class LibraryService {
} }
} }
if (validImportPaths) { if (validImportPaths.length === 0) {
const assetsOnDisk = this.storageRepository.walk({
pathsToCrawl: validImportPaths,
includeHidden: false,
exclusionPatterns: library.exclusionPatterns,
take: JOBS_LIBRARY_PAGINATION_SIZE,
});
let count = 0;
for await (const assetBatch of assetsOnDisk) {
count += assetBatch.length;
this.logger.debug(`Discovered ${count} asset(s) on disk for library ${library.id}...`);
await this.syncFiles(library, assetBatch);
this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`);
}
if (count > 0) {
this.logger.debug(`Finished queueing scan of ${count} assets on disk for library ${library.id}`);
} else {
this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`);
}
} else {
this.logger.warn(`No valid import paths found for library ${library.id}`); this.logger.warn(`No valid import paths found for library ${library.id}`);
} }
await this.repository.update({ id: job.id, refreshedAt: new Date() }); const assetsOnDisk = this.storageRepository.walk({
pathsToCrawl: validImportPaths,
includeHidden: false,
exclusionPatterns: library.exclusionPatterns,
take: JOBS_LIBRARY_PAGINATION_SIZE,
});
return JobStatus.SUCCESS; let crawledAssets = 0;
}
async handleQueueSyncAssets(job: IEntityJob): Promise<JobStatus> { for await (const assetBatch of assetsOnDisk) {
const library = await this.repository.get(job.id); crawledAssets += assetBatch.length;
if (!library) { this.logger.debug(`Discovered ${crawledAssets} asset(s) on disk for library ${library.id}...`);
return JobStatus.SKIPPED; await this.scanAssets(job.id, assetBatch, library.ownerId, job.refreshAllFiles ?? false);
this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`);
} }
this.logger.log(`Scanning library ${library.id} for removed assets`); if (crawledAssets) {
this.logger.debug(`Finished queueing scan of ${crawledAssets} assets on disk for library ${library.id}`);
} else {
this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`);
}
const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getAll(pagination, { libraryId: job.id }), this.assetRepository.getWith(pagination, WithProperty.IS_ONLINE, job.id),
); );
let assetCount = 0; let onlineAssetCount = 0;
for await (const assets of onlineAssets) { for await (const assets of onlineAssets) {
assetCount += assets.length; onlineAssetCount += assets.length;
this.logger.debug(`Discovered ${assetCount} asset(s) in library ${library.id}...`); this.logger.debug(`Discovered ${onlineAssetCount} asset(s) in library ${library.id}...`);
await this.jobRepository.queueAll( await this.jobRepository.queueAll(
assets.map((asset) => ({ assets.map((asset) => ({
name: JobName.LIBRARY_SYNC_ASSET, name: JobName.LIBRARY_CHECK_OFFLINE,
data: { id: asset.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns }, data: { id: asset.id, importPaths: validImportPaths, exclusionPatterns: library.exclusionPatterns },
})), })),
); );
this.logger.debug(`Queued check of ${assets.length} asset(s) in library ${library.id}...`); this.logger.debug(`Queued online check of ${assets.length} asset(s) in library ${library.id}...`);
} }
if (assetCount) { if (onlineAssetCount) {
this.logger.log(`Finished queueing check of ${assetCount} assets for library ${library.id}`); this.logger.log(`Finished queueing online check of ${onlineAssetCount} assets for library ${library.id}`);
} }
await this.repository.update({ id: job.id, refreshedAt: new Date() });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
+5 -5
View File
@@ -86,12 +86,12 @@ export class MicroservicesService {
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data), [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
[JobName.SIDECAR_SYNC]: (data) => this.metadataService.handleSidecarSync(data), [JobName.SIDECAR_SYNC]: (data) => this.metadataService.handleSidecarSync(data),
[JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data), [JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data),
[JobName.LIBRARY_QUEUE_SYNC_ALL]: () => this.libraryService.handleQueueSyncAll(), [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data),
[JobName.LIBRARY_QUEUE_SYNC_FILES]: (data) => this.libraryService.handleQueueSyncFiles(data), //Queues all files paths on disk [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data),
[JobName.LIBRARY_SYNC_FILE]: (data) => this.libraryService.handleSyncFile(data), //Handles a single path on disk //Watcher calls for new files
[JobName.LIBRARY_QUEUE_SYNC_ASSETS]: (data) => this.libraryService.handleQueueSyncAssets(data), //Queues all library assets
[JobName.LIBRARY_SYNC_ASSET]: (data) => this.libraryService.handleSyncAsset(data), //Handles all library assets // Watcher calls for unlink and changed
[JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data), [JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data),
[JobName.LIBRARY_CHECK_OFFLINE]: (data) => this.libraryService.handleOfflineCheck(data),
[JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleRemoveOffline(data),
[JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data),
[JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(), [JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(),
[JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data), [JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data),
[JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data), [JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data),
@@ -616,6 +616,11 @@ describe(NotificationService.name, () => {
await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SKIPPED);
}); });
it('should fail if email could not be sent', async () => {
systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true } } });
await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.FAILED);
});
it('should send mail successfully', async () => { it('should send mail successfully', async () => {
systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app' } } }); systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app' } } });
notificationMock.sendEmail.mockResolvedValue({ messageId: '', response: '' }); notificationMock.sendEmail.mockResolvedValue({ messageId: '', response: '' });
+7 -5
View File
@@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit } from 'src/decorators'; import { OnEmit } from 'src/decorators';
@@ -140,7 +140,7 @@ export class NotificationService {
try { try {
await this.notificationRepository.verifySmtp(dto.transport); await this.notificationRepository.verifySmtp(dto.transport);
} catch (error) { } catch (error) {
throw new BadRequestException('Failed to verify SMTP configuration', { cause: error }); throw new HttpException('Failed to verify SMTP configuration', HttpStatus.BAD_REQUEST, { cause: error });
} }
const { server } = await this.configCore.getConfig({ withCache: false }); const { server } = await this.configCore.getConfig({ withCache: false });
@@ -152,7 +152,7 @@ export class NotificationService {
}, },
}); });
const { messageId } = await this.notificationRepository.sendEmail({ await this.notificationRepository.sendEmail({
to: user.email, to: user.email,
subject: 'Test email from Immich', subject: 'Test email from Immich',
html, html,
@@ -161,8 +161,6 @@ export class NotificationService {
replyTo: dto.replyTo || dto.from, replyTo: dto.replyTo || dto.from,
smtp: dto.transport, smtp: dto.transport,
}); });
return { messageId };
} }
async handleUserSignup({ id, tempPassword }: INotifySignupJob) { async handleUserSignup({ id, tempPassword }: INotifySignupJob) {
@@ -314,6 +312,10 @@ export class NotificationService {
imageAttachments: data.imageAttachments, imageAttachments: data.imageAttachments,
}); });
if (!response) {
return JobStatus.FAILED;
}
this.logger.log(`Sent mail with id: ${response.messageId} status: ${response.response}`); this.logger.log(`Sent mail with id: ${response.messageId} status: ${response.response}`);
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
+2 -2
View File
@@ -67,7 +67,7 @@ describe(TrashService.name, () => {
}); });
it('should restore', async () => { it('should restore', async () => {
trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-id'], hasNextPage: false });
trashMock.restore.mockResolvedValue(1); trashMock.restore.mockResolvedValue(1);
await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 }); await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 });
expect(trashMock.restore).toHaveBeenCalledWith('user-id'); expect(trashMock.restore).toHaveBeenCalledWith('user-id');
@@ -83,7 +83,7 @@ describe(TrashService.name, () => {
}); });
it('should empty the trash', async () => { it('should empty the trash', async () => {
trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-id'], hasNextPage: false });
trashMock.empty.mockResolvedValue(1); trashMock.empty.mockResolvedValue(1);
await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 }); await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 });
expect(trashMock.empty).toHaveBeenCalledWith('user-id'); expect(trashMock.empty).toHaveBeenCalledWith('user-id');
+1 -1
View File
@@ -80,7 +80,7 @@ export function searchAssetBuilder(
}); });
} }
const status = _.pick(options, ['isFavorite', 'isVisible', 'type']); const status = _.pick(options, ['isFavorite', 'isOffline', 'isVisible', 'type']);
const { const {
isArchived, isArchived,
isEncoded, isEncoded,
+133 -59
View File
@@ -70,9 +70,9 @@ export const assetStub = {
faces: [], faces: [],
sidecarPath: null, sidecarPath: null,
deletedAt: null, deletedAt: null,
isOffline: false,
isExternal: false, isExternal: false,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
noWebpPath: Object.freeze<AssetEntity>({ noWebpPath: Object.freeze<AssetEntity>({
@@ -104,13 +104,13 @@ export const assetStub = {
originalFileName: 'IMG_456.jpg', originalFileName: 'IMG_456.jpg',
faces: [], faces: [],
sidecarPath: null, sidecarPath: null,
isOffline: false,
isExternal: false, isExternal: false,
exifInfo: { exifInfo: {
fileSizeInByte: 123_000, fileSizeInByte: 123_000,
} as ExifEntity, } as ExifEntity,
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
noThumbhash: Object.freeze<AssetEntity>({ noThumbhash: Object.freeze<AssetEntity>({
@@ -133,6 +133,7 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'), localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true, isFavorite: true,
isArchived: false, isArchived: false,
isOffline: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
isExternal: false, isExternal: false,
@@ -145,7 +146,6 @@ export const assetStub = {
sidecarPath: null, sidecarPath: null,
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
primaryImage: Object.freeze<AssetEntity>({ primaryImage: Object.freeze<AssetEntity>({
@@ -173,6 +173,7 @@ export const assetStub = {
isExternal: false, isExternal: false,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@@ -190,7 +191,6 @@ export const assetStub = {
{ id: 'stack-child-asset-2' } as AssetEntity, { id: 'stack-child-asset-2' } as AssetEntity,
]), ]),
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
image: Object.freeze<AssetEntity>({ image: Object.freeze<AssetEntity>({
@@ -218,6 +218,7 @@ export const assetStub = {
isExternal: false, isExternal: false,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@@ -230,50 +231,9 @@ export const assetStub = {
exifImageWidth: 2160, exifImageWidth: 2160,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
trashed: Object.freeze<AssetEntity>({ trashed: Object.freeze<AssetEntity>({
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,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
exifImageHeight: 3840,
exifImageWidth: 2160,
} as ExifEntity,
duplicateId: null,
isOffline: false,
status: AssetStatus.TRASHED,
}),
trashedOffline: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE, status: AssetStatus.ACTIVE,
deviceAssetId: 'device-asset-id', deviceAssetId: 'device-asset-id',
@@ -299,6 +259,7 @@ export const assetStub = {
isExternal: false, isExternal: false,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@@ -310,8 +271,8 @@ export const assetStub = {
exifImageWidth: 2160, exifImageWidth: 2160,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: true,
}), }),
archived: Object.freeze<AssetEntity>({ archived: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
status: AssetStatus.ACTIVE, status: AssetStatus.ACTIVE,
@@ -337,6 +298,7 @@ export const assetStub = {
isExternal: false, isExternal: false,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@@ -349,7 +311,6 @@ export const assetStub = {
exifImageWidth: 2160, exifImageWidth: 2160,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
external: Object.freeze<AssetEntity>({ external: Object.freeze<AssetEntity>({
@@ -377,6 +338,7 @@ export const assetStub = {
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
libraryId: 'library-id', libraryId: 'library-id',
library: libraryStub.externalLibrary1, library: libraryStub.externalLibrary1,
tags: [], tags: [],
@@ -389,7 +351,84 @@ export const assetStub = {
fileSizeInByte: 5000, fileSizeInByte: 5000,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: false, }),
offline: Object.freeze<AssetEntity>({
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,
isArchived: false,
isExternal: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
isOffline: true,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
} as ExifEntity,
deletedAt: null,
duplicateId: null,
}),
externalOffline: Object.freeze<AssetEntity>({
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,
isArchived: false,
isExternal: true,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
isOffline: true,
libraryId: 'library-id',
library: libraryStub.externalLibrary1,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
} as ExifEntity,
deletedAt: null,
duplicateId: null,
}), }),
image1: Object.freeze<AssetEntity>({ image1: Object.freeze<AssetEntity>({
@@ -418,6 +457,7 @@ export const assetStub = {
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isExternal: false, isExternal: false,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.ext', originalFileName: 'asset-id.ext',
@@ -427,7 +467,6 @@ export const assetStub = {
fileSizeInByte: 5000, fileSizeInByte: 5000,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
imageFrom2015: Object.freeze<AssetEntity>({ imageFrom2015: Object.freeze<AssetEntity>({
@@ -451,6 +490,7 @@ export const assetStub = {
isFavorite: true, isFavorite: true,
isArchived: false, isArchived: false,
isExternal: false, isExternal: false,
isOffline: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@@ -465,7 +505,6 @@ export const assetStub = {
} as ExifEntity, } as ExifEntity,
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
video: Object.freeze<AssetEntity>({ video: Object.freeze<AssetEntity>({
@@ -490,6 +529,7 @@ export const assetStub = {
isFavorite: true, isFavorite: true,
isArchived: false, isArchived: false,
isExternal: false, isExternal: false,
isOffline: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@@ -505,7 +545,6 @@ export const assetStub = {
} as ExifEntity, } as ExifEntity,
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
livePhotoMotionAsset: Object.freeze({ livePhotoMotionAsset: Object.freeze({
@@ -625,6 +664,7 @@ export const assetStub = {
isFavorite: false, isFavorite: false,
isArchived: false, isArchived: false,
isExternal: false, isExternal: false,
isOffline: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@@ -643,7 +683,6 @@ export const assetStub = {
} as ExifEntity, } as ExifEntity,
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
sidecar: Object.freeze<AssetEntity>({ sidecar: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
@@ -666,6 +705,7 @@ export const assetStub = {
isFavorite: true, isFavorite: true,
isArchived: false, isArchived: false,
isExternal: false, isExternal: false,
isOffline: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@@ -677,7 +717,6 @@ export const assetStub = {
sidecarPath: '/original/path.ext.xmp', sidecarPath: '/original/path.ext.xmp',
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
sidecarWithoutExt: Object.freeze<AssetEntity>({ sidecarWithoutExt: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
@@ -700,6 +739,7 @@ export const assetStub = {
isFavorite: true, isFavorite: true,
isArchived: false, isArchived: false,
isExternal: false, isExternal: false,
isOffline: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@@ -711,7 +751,41 @@ export const assetStub = {
sidecarPath: '/original/path.xmp', sidecarPath: '/original/path.xmp',
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
}),
readOnly: Object.freeze<AssetEntity>({
id: 'read-only-asset',
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',
thumbhash: null,
checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE,
files: [previewFile],
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,
isArchived: false,
isExternal: false,
isOffline: false, isOffline: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.ext',
faces: [],
sidecarPath: '/original/path.ext.xmp',
deletedAt: null,
duplicateId: null,
}), }),
hasEncodedVideo: Object.freeze<AssetEntity>({ hasEncodedVideo: Object.freeze<AssetEntity>({
@@ -736,6 +810,7 @@ export const assetStub = {
isFavorite: true, isFavorite: true,
isArchived: false, isArchived: false,
isExternal: false, isExternal: false,
isOffline: false,
duration: null, duration: null,
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
@@ -749,7 +824,6 @@ export const assetStub = {
} as ExifEntity, } as ExifEntity,
deletedAt: null, deletedAt: null,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
missingFileExtension: Object.freeze<AssetEntity>({ missingFileExtension: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
@@ -776,6 +850,7 @@ export const assetStub = {
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
libraryId: 'library-id', libraryId: 'library-id',
library: libraryStub.externalLibrary1, library: libraryStub.externalLibrary1,
tags: [], tags: [],
@@ -788,7 +863,6 @@ export const assetStub = {
fileSizeInByte: 5000, fileSizeInByte: 5000,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
hasFileExtension: Object.freeze<AssetEntity>({ hasFileExtension: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
@@ -815,6 +889,7 @@ export const assetStub = {
isVisible: true, isVisible: true,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
libraryId: 'library-id', libraryId: 'library-id',
library: libraryStub.externalLibrary1, library: libraryStub.externalLibrary1,
tags: [], tags: [],
@@ -827,7 +902,6 @@ export const assetStub = {
fileSizeInByte: 5000, fileSizeInByte: 5000,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
imageDng: Object.freeze<AssetEntity>({ imageDng: Object.freeze<AssetEntity>({
id: 'asset-id', id: 'asset-id',
@@ -854,6 +928,7 @@ export const assetStub = {
isExternal: false, isExternal: false,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@@ -866,7 +941,6 @@ export const assetStub = {
bitsPerSample: 14, bitsPerSample: 14,
} as ExifEntity, } as ExifEntity,
duplicateId: null, duplicateId: null,
isOffline: false,
}), }),
hasEmbedding: Object.freeze<AssetEntity>({ hasEmbedding: Object.freeze<AssetEntity>({
id: 'asset-id-embedding', id: 'asset-id-embedding',
@@ -893,6 +967,7 @@ export const assetStub = {
isExternal: false, isExternal: false,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@@ -907,7 +982,6 @@ export const assetStub = {
assetId: 'asset-id', assetId: 'asset-id',
embedding: Array.from({ length: 512 }, Math.random), embedding: Array.from({ length: 512 }, Math.random),
}, },
isOffline: false,
}), }),
hasDupe: Object.freeze<AssetEntity>({ hasDupe: Object.freeze<AssetEntity>({
id: 'asset-id-dupe', id: 'asset-id-dupe',
@@ -934,6 +1008,7 @@ export const assetStub = {
isExternal: false, isExternal: false,
livePhotoVideo: null, livePhotoVideo: null,
livePhotoVideoId: null, livePhotoVideoId: null,
isOffline: false,
tags: [], tags: [],
sharedLinks: [], sharedLinks: [],
originalFileName: 'asset-id.jpg', originalFileName: 'asset-id.jpg',
@@ -948,6 +1023,5 @@ export const assetStub = {
assetId: 'asset-id', assetId: 'asset-id',
embedding: Array.from({ length: 512 }, Math.random), embedding: Array.from({ length: 512 }, Math.random),
}, },
isOffline: false,
}), }),
}; };
@@ -25,6 +25,7 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
getLivePhotoCount: vitest.fn(), getLivePhotoCount: vitest.fn(),
updateAll: vitest.fn(), updateAll: vitest.fn(),
updateDuplicates: vitest.fn(), updateDuplicates: vitest.fn(),
getExternalLibraryAssetPaths: vitest.fn(),
getByLibraryIdAndOriginalPath: vitest.fn(), getByLibraryIdAndOriginalPath: vitest.fn(),
deleteAll: vitest.fn(), deleteAll: vitest.fn(),
update: vitest.fn(), update: vitest.fn(),
@@ -4,7 +4,7 @@ import { Mocked } from 'vitest';
export const newNotificationRepositoryMock = (): Mocked<INotificationRepository> => { export const newNotificationRepositoryMock = (): Mocked<INotificationRepository> => {
return { return {
renderEmail: vitest.fn(), renderEmail: vitest.fn(),
sendEmail: vitest.fn().mockResolvedValue({ messageId: 'message-1' }), sendEmail: vitest.fn(),
verifySmtp: vitest.fn(), verifySmtp: vitest.fn(),
}; };
}; };
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
lines: 80, lines: 80,
statements: 80, statements: 80,
branches: 85, branches: 85,
functions: 80, functions: 85,
}, },
}, },
server: { server: {
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.116.0", "version": "1.115.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-web", "name": "immich-web",
"version": "1.116.0", "version": "1.115.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@formatjs/icu-messageformat-parser": "^2.7.8", "@formatjs/icu-messageformat-parser": "^2.7.8",
@@ -74,7 +74,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.115.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.116.0", "version": "1.115.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"scripts": { "scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000", "dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -75,6 +75,21 @@
</FormatMessage> </FormatMessage>
</p> </p>
</SettingInputField> </SettingInputField>
<SettingAccordion
key="Preload clip model"
title={$t('admin.machine_learning_preload_model')}
subtitle={$t('admin.machine_learning_preload_model_setting_description')}
>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title={$t('admin.machine_learning_preload_model_enabled')}
subtitle={$t('admin.machine_learning_preload_model_enabled_description')}
bind:checked={config.machineLearning.clip.loadTextualModelOnConnection.enabled}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
/>
</div>
</SettingAccordion>
</div> </div>
</SettingAccordion> </SettingAccordion>
@@ -59,6 +59,7 @@
export let onClose: () => void; export let onClose: () => void;
const sharedLink = getSharedLink(); const sharedLink = getSharedLink();
$: isOwner = $user && asset.ownerId === $user?.id; $: isOwner = $user && asset.ownerId === $user?.id;
$: showDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline; $: showDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
// $: showEditorButton = // $: showEditorButton =
@@ -86,7 +87,7 @@
<ShareAction {asset} /> <ShareAction {asset} />
{/if} {/if}
{#if asset.isOffline} {#if asset.isOffline}
<CircleIconButton color="alert" icon={mdiAlertOutline} on:click={onShowDetail} title={$t('asset_offline')} /> <CircleIconButton color="opaque" icon={mdiAlertOutline} on:click={onShowDetail} title={$t('asset_offline')} />
{/if} {/if}
{#if asset.livePhotoVideoId} {#if asset.livePhotoVideoId}
<slot name="motion-photo" /> <slot name="motion-photo" />
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte'; import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { isSharedLink } from '$lib/utils'; import { isSharedLink } from '$lib/utils';
import { removeTag, tagAssets } from '$lib/utils/asset-utils'; import { removeTag, tagAssets } from '$lib/utils/asset-utils';
@@ -77,7 +76,5 @@
{/if} {/if}
{#if isOpen} {#if isOpen}
<Portal> <TagAssetForm onTag={(tagsIds) => handleTag(tagsIds)} onCancel={handleCancel} />
<TagAssetForm onTag={(tagsIds) => handleTag(tagsIds)} onCancel={handleCancel} />
</Portal>
{/if} {/if}
@@ -44,7 +44,6 @@
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte';
import AlbumListItemDetails from './album-list-item-details.svelte'; import AlbumListItemDetails from './album-list-item-details.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
@@ -148,21 +147,12 @@
{#if asset.isOffline} {#if asset.isOffline}
<section class="px-4 py-4"> <section class="px-4 py-4">
<div role="alert"> <div role="alert">
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white"> <div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">{$t('asset_offline')}</div>
{$t('asset_offline')} <div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
</div>
<div class="border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
<p> <p>
{#if $user?.isAdmin} {$t('asset_offline_description')}
<p>{$t('admin.asset_offline_description')}</p>
{:else}
{$t('asset_offline_description')}
{/if}
</p> </p>
</div> </div>
<div class="rounded-b bg-red-500 px-4 py-2 text-white text-sm">
<p>{asset.originalPath}</p>
</div>
</div> </div>
</section> </section>
{/if} {/if}
@@ -335,14 +325,12 @@
{/if} {/if}
{#if isShowChangeDate} {#if isShowChangeDate}
<Portal> <ChangeDate
<ChangeDate initialDate={dateTime}
initialDate={dateTime} initialTimeZone={timeZone ?? ''}
initialTimeZone={timeZone ?? ''} onConfirm={handleConfirmChangeDate}
onConfirm={handleConfirmChangeDate} onCancel={() => (isShowChangeDate = false)}
onCancel={() => (isShowChangeDate = false)} />
/>
</Portal>
{/if} {/if}
{#if asset.exifInfo?.fileSizeInByte} {#if asset.exifInfo?.fileSizeInByte}
@@ -1,7 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { HTMLButtonAttributes, HTMLLinkAttributes } from 'svelte/elements'; import type { HTMLButtonAttributes, HTMLLinkAttributes } from 'svelte/elements';
export type Color = 'transparent' | 'light' | 'dark' | 'gray' | 'primary' | 'opaque' | 'alert'; export type Color = 'transparent' | 'light' | 'dark' | 'gray' | 'primary' | 'opaque';
export type Padding = '1' | '2' | '3'; export type Padding = '1' | '2' | '3';
type BaseProps = { type BaseProps = {
@@ -65,7 +65,6 @@
opaque: 'bg-transparent hover:bg-immich-bg/30 text-white hover:dark:text-white', opaque: 'bg-transparent hover:bg-immich-bg/30 text-white hover:dark:text-white',
light: 'bg-white hover:bg-[#d3d3d3]', light: 'bg-white hover:bg-[#d3d3d3]',
dark: 'bg-[#202123] hover:bg-[#d3d3d3]', dark: 'bg-[#202123] hover:bg-[#d3d3d3]',
alert: 'text-[#ff0000] hover:text-white',
gray: 'bg-[#d3d3d3] hover:bg-[#e2e7e9] text-immich-dark-gray hover:text-black', gray: 'bg-[#d3d3d3] hover:bg-[#e2e7e9] text-immich-dark-gray hover:text-black',
primary: primary:
'bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 hover:dark:bg-immich-dark-primary/80 text-white dark:text-immich-dark-gray', 'bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 hover:dark:bg-immich-dark-primary/80 text-white dark:text-immich-dark-gray',
@@ -21,7 +21,7 @@
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js'; import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js';
import { onMount, tick } from 'svelte'; import { tick } from 'svelte';
import type { FormEventHandler } from 'svelte/elements'; import type { FormEventHandler } from 'svelte/elements';
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import { focusOutside } from '$lib/actions/focus-outside'; import { focusOutside } from '$lib/actions/focus-outside';
@@ -53,28 +53,8 @@
let selectedIndex: number | undefined; let selectedIndex: number | undefined;
let optionRefs: HTMLElement[] = []; let optionRefs: HTMLElement[] = [];
let input: HTMLInputElement; let input: HTMLInputElement;
let bounds: DOMRect | undefined;
let dropdownDirection: 'bottom' | 'top' = 'bottom';
const inputId = `combobox-${id}`; const inputId = `combobox-${id}`;
const listboxId = `listbox-${id}`; const listboxId = `listbox-${id}`;
/**
* Buffer distance between the dropdown and top/bottom of the viewport.
*/
const dropdownOffset = 15;
/**
* Minimum space required for the dropdown to be displayed at the bottom of the input.
*/
const bottomBreakpoint = 225;
const observer = new IntersectionObserver(
(entries) => {
const inputEntry = entries[0];
if (inputEntry.intersectionRatio < 1) {
isOpen = false;
}
},
{ threshold: 0.5 },
);
$: filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())); $: filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
@@ -82,23 +62,6 @@
searchQuery = selectedOption ? selectedOption.label : ''; searchQuery = selectedOption ? selectedOption.label : '';
} }
$: position = calculatePosition(bounds);
onMount(() => {
observer.observe(input);
const scrollableAncestor = input.closest('.overflow-y-auto, .overflow-y-scroll');
scrollableAncestor?.addEventListener('scroll', onPositionChange);
window.visualViewport?.addEventListener('resize', onPositionChange);
window.visualViewport?.addEventListener('scroll', onPositionChange);
return () => {
observer.disconnect();
scrollableAncestor?.removeEventListener('scroll', onPositionChange);
window.visualViewport?.removeEventListener('resize', onPositionChange);
window.visualViewport?.removeEventListener('scroll', onPositionChange);
};
});
const activate = () => { const activate = () => {
isActive = true; isActive = true;
searchQuery = ''; searchQuery = '';
@@ -113,7 +76,6 @@
const openDropdown = () => { const openDropdown = () => {
isOpen = true; isOpen = true;
bounds = getInputPosition();
}; };
const closeDropdown = () => { const closeDropdown = () => {
@@ -154,67 +116,8 @@
searchQuery = ''; searchQuery = '';
onSelect(selectedOption); onSelect(selectedOption);
}; };
const calculatePosition = (boundary: DOMRect | undefined) => {
const visualViewport = window.visualViewport;
dropdownDirection = getComboboxDirection(boundary, visualViewport);
if (!boundary) {
return;
}
const left = boundary.left + (visualViewport?.offsetLeft || 0);
const offsetTop = visualViewport?.offsetTop || 0;
if (dropdownDirection === 'top') {
return {
bottom: `${window.innerHeight - boundary.top - offsetTop}px`,
left: `${left}px`,
width: `${boundary.width}px`,
maxHeight: maxHeight(boundary.top - dropdownOffset),
};
}
const viewportHeight = visualViewport?.height || 0;
const availableHeight = viewportHeight - boundary.bottom;
return {
top: `${boundary.bottom + offsetTop}px`,
left: `${left}px`,
width: `${boundary.width}px`,
maxHeight: maxHeight(availableHeight - dropdownOffset),
};
};
const maxHeight = (size: number) => `min(${size}px,18rem)`;
const onPositionChange = () => {
if (!isOpen) {
return;
}
bounds = getInputPosition();
};
const getComboboxDirection = (
boundary: DOMRect | undefined,
visualViewport: VisualViewport | null,
): 'bottom' | 'top' => {
if (!boundary) {
return 'bottom';
}
const visualHeight = visualViewport?.height || 0;
const heightBelow = visualHeight - boundary.bottom;
const heightAbove = boundary.top;
const isViewportScaled = visualHeight && Math.floor(visualHeight) !== Math.floor(window.innerHeight);
return heightBelow <= bottomBreakpoint && heightAbove > heightBelow && !isViewportScaled ? 'top' : 'bottom';
};
const getInputPosition = () => input?.getBoundingClientRect();
</script> </script>
<svelte:window on:resize={onPositionChange} />
<label class="immich-form-label" class:sr-only={hideLabel} for={inputId}>{label}</label> <label class="immich-form-label" class:sr-only={hideLabel} for={inputId}>{label}</label>
<div <div
class="relative w-full dark:text-gray-300 text-gray-700 text-base" class="relative w-full dark:text-gray-300 text-gray-700 text-base"
@@ -247,8 +150,7 @@
autocomplete="off" autocomplete="off"
bind:this={input} bind:this={input}
class:!pl-8={isActive} class:!pl-8={isActive}
class:!rounded-b-none={isOpen && dropdownDirection === 'bottom'} class:!rounded-b-none={isOpen}
class:!rounded-t-none={isOpen && dropdownDirection === 'top'}
class:cursor-pointer={!isActive} class:cursor-pointer={!isActive}
class="immich-form-input text-sm text-left w-full !pr-12 transition-all" class="immich-form-input text-sm text-left w-full !pr-12 transition-all"
id={inputId} id={inputId}
@@ -315,16 +217,8 @@
role="listbox" role="listbox"
id={listboxId} id={listboxId}
transition:fly={{ duration: 250 }} transition:fly={{ duration: 250 }}
class="fixed text-left text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900 z-[10000]" class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-[10000]"
class:rounded-b-xl={dropdownDirection === 'bottom'}
class:rounded-t-xl={dropdownDirection === 'top'}
class:shadow={dropdownDirection === 'bottom'}
class:border={isOpen} class:border={isOpen}
style:top={position?.top}
style:bottom={position?.bottom}
style:left={position?.left}
style:width={position?.width}
style:max-height={position?.maxHeight}
tabindex="-1" tabindex="-1"
> >
{#if isOpen} {#if isOpen}
@@ -334,7 +228,7 @@
role="option" role="option"
aria-selected={selectedIndex === 0} aria-selected={selectedIndex === 0}
aria-disabled={true} aria-disabled={true}
class="text-left w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700" class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700"
id={`${listboxId}-${0}`} id={`${listboxId}-${0}`}
on:click={() => closeDropdown()} on:click={() => closeDropdown()}
> >
@@ -346,7 +240,7 @@
<li <li
aria-selected={index === selectedIndex} aria-selected={index === selectedIndex}
bind:this={optionRefs[index]} bind:this={optionRefs[index]}
class="text-left w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700 break-words" class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700"
id={`${listboxId}-${index}`} id={`${listboxId}-${index}`}
on:click={() => handleSelect(option)} on:click={() => handleSelect(option)}
role="option" role="option"
@@ -68,24 +68,28 @@
use:focusTrap use:focusTrap
> >
<div <div
class="flex flex-col max-h-[min(95dvh,60rem)] z-[9999] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4" class="z-[9999] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4"
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }} use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
tabindex="-1" tabindex="-1"
aria-modal="true" aria-modal="true"
aria-labelledby={titleId} aria-labelledby={titleId}
> >
<div class="immich-scrollbar overflow-y-auto pt-1" class:pb-4={isStickyBottom}> <div
class="immich-scrollbar overflow-y-auto max-h-[min(92dvh,64rem)] py-1"
class:scroll-pb-40={isStickyBottom}
class:sm:scroll-p-24={isStickyBottom}
>
<ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} /> <ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
<div class="px-5 pt-0"> <div class="px-5 pt-0">
<slot /> <slot />
</div> </div>
{#if isStickyBottom}
<div
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky -bottom-[4px] py-2 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500 shadow z-[9999]"
>
<slot name="sticky-bottom" />
</div>
{/if}
</div> </div>
{#if isStickyBottom}
<div
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500"
>
<slot name="sticky-bottom" />
</div>
{/if}
</div> </div>
</section> </section>
+3 -3
View File
@@ -198,7 +198,7 @@
"refreshing_all_libraries": "تحديث كافة المكتبات", "refreshing_all_libraries": "تحديث كافة المكتبات",
"registration": "تسجيل المدير", "registration": "تسجيل المدير",
"registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.", "registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.",
"removing_deleted_files": "إزالة الملفات غير المتصلة", "removing_offline_files": "إزالة الملفات غير المتصلة",
"repair_all": "إصلاح الكل", "repair_all": "إصلاح الكل",
"repair_matched_items": "تمت مطابقة {count, plural, one {# عنصر} other {# عناصر}}", "repair_matched_items": "تمت مطابقة {count, plural, one {# عنصر} other {# عناصر}}",
"repaired_items": "تم إصلاح {count, plural, one {# عنصر} other {# عناصر}}", "repaired_items": "تم إصلاح {count, plural, one {# عنصر} other {# عناصر}}",
@@ -671,8 +671,8 @@
"unable_to_remove_api_key": "تعذر إزالة مفتاح API", "unable_to_remove_api_key": "تعذر إزالة مفتاح API",
"unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك", "unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "غير قادر على إزالة الملفات غير المتصلة",
"unable_to_remove_library": "غير قادر على إزالة المكتبة", "unable_to_remove_library": "غير قادر على إزالة المكتبة",
"unable_to_remove_offline_files": "غير قادر على إزالة الملفات غير المتصلة",
"unable_to_remove_partner": "غير قادر على إزالة الشريك", "unable_to_remove_partner": "غير قادر على إزالة الشريك",
"unable_to_remove_reaction": "غير قادر على إزالة رد الفعل", "unable_to_remove_reaction": "غير قادر على إزالة رد الفعل",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -1072,10 +1072,10 @@
"remove_assets_shared_link_confirmation": "هل أنت متأكد أنك تريد إزالة {count, plural, one {# المحتوى} other {# المحتويات}} من رابط المشاركة هذا؟", "remove_assets_shared_link_confirmation": "هل أنت متأكد أنك تريد إزالة {count, plural, one {# المحتوى} other {# المحتويات}} من رابط المشاركة هذا؟",
"remove_assets_title": "هل تريد إزالة المحتويات؟", "remove_assets_title": "هل تريد إزالة المحتويات؟",
"remove_custom_date_range": "إزالة النطاق الزمني المخصص", "remove_custom_date_range": "إزالة النطاق الزمني المخصص",
"remove_deleted_assets": "إزالة الملفات الغير متصلة",
"remove_from_album": "إزالة من الألبوم", "remove_from_album": "إزالة من الألبوم",
"remove_from_favorites": "إزالة من المفضلة", "remove_from_favorites": "إزالة من المفضلة",
"remove_from_shared_link": "إزالة من الرابط المشترك", "remove_from_shared_link": "إزالة من الرابط المشترك",
"remove_offline_files": "إزالة الملفات الغير متصلة",
"remove_user": "إزالة المستخدم", "remove_user": "إزالة المستخدم",
"removed_api_key": "تم إزالة مفتاح API: {name}", "removed_api_key": "تم إزالة مفتاح API: {name}",
"removed_from_archive": "تمت إزالتها من الأرشيف", "removed_from_archive": "تمت إزالتها من الأرشيف",
+10 -10
View File
@@ -12,19 +12,19 @@
"add_a_description": "Добави описание", "add_a_description": "Добави описание",
"add_a_location": "Добави местоположение", "add_a_location": "Добави местоположение",
"add_a_name": "Добави име", "add_a_name": "Добави име",
"add_a_title": "Добавете заглавие", "add_a_title": "Добави заглавие",
"add_exclusion_pattern": "Добави модел за изключване", "add_exclusion_pattern": "Добави модел за изключване",
"add_import_path": "Добави път за импортиране", "add_import_path": "Добави път за импортиране",
"add_location": "Добавете местоположение", "add_location": "Добави местоположение",
"add_more_users": "Добавете още потребители", "add_more_users": "Добави още потребители",
"add_partner": "Добавете партньор", "add_partner": "Добави партньор",
"add_path": "Добави път", "add_path": "Добави път",
"add_photos": "Добавете снимки", "add_photos": "Добави снимки",
"add_to": "Добави към...", "add_to": "Добави към...",
"add_to_album": "Добави към албум", "add_to_album": "Добави към албум",
"add_to_shared_album": "Добави към споделен албум", "add_to_shared_album": "Добави към споделен албум",
"added_to_archive": "Добавено към архива", "added_to_archive": "Добавено в архива",
"added_to_favorites": "Добавени към любимите ви", "added_to_favorites": "Добавено към любими",
"added_to_favorites_count": "Добавени {count, number} към любими", "added_to_favorites_count": "Добавени {count, number} към любими",
"admin": { "admin": {
"add_exclusion_pattern_description": "Добави модели за изключване. Поддържа се \"globbing\" с помощта на *, ** и ?. За да игнорирате всички файлове в директория с име \"Raw\", използвайте \"**/Raw/**\". За да игнорирате всички файлове, завършващи на \".tif\", използвайте \"**/*.tif\". За да игнорирате абсолютен път, използвайте \"/path/to/ignore/**\".", "add_exclusion_pattern_description": "Добави модели за изключване. Поддържа се \"globbing\" с помощта на *, ** и ?. За да игнорирате всички файлове в директория с име \"Raw\", използвайте \"**/Raw/**\". За да игнорирате всички файлове, завършващи на \".tif\", използвайте \"**/*.tif\". За да игнорирате абсолютен път, използвайте \"/path/to/ignore/**\".",
@@ -200,7 +200,7 @@
"refreshing_all_libraries": "Опресняване на всички библиотеки", "refreshing_all_libraries": "Опресняване на всички библиотеки",
"registration": "Администраторска регистрация", "registration": "Администраторска регистрация",
"registration_description": "Тъй като сте първият потребител в системата, ще бъдете назначен като администратор и ще отговаряте за административните задачи, а допълнителните потребители ще бъдат създадени от вас.", "registration_description": "Тъй като сте първият потребител в системата, ще бъдете назначен като администратор и ще отговаряте за административните задачи, а допълнителните потребители ще бъдат създадени от вас.",
"removing_deleted_files": "Премахване на офлайн файлове", "removing_offline_files": "Премахване на офлайн файлове",
"repair_all": "Поправяне на всичко", "repair_all": "Поправяне на всичко",
"repair_matched_items": "{count, plural, one {Съвпадащ елемент (#)} other {Съвпадащи елементи (#)}}", "repair_matched_items": "{count, plural, one {Съвпадащ елемент (#)} other {Съвпадащи елементи (#)}}",
"repaired_items": "{count, plural, one {Поправен елемент (#)} other {Поправени елементи (#)}}", "repaired_items": "{count, plural, one {Поправен елемент (#)} other {Поправени елементи (#)}}",
@@ -605,8 +605,8 @@
"unable_to_refresh_user": "", "unable_to_refresh_user": "",
"unable_to_remove_album_users": "", "unable_to_remove_album_users": "",
"unable_to_remove_api_key": "", "unable_to_remove_api_key": "",
"unable_to_remove_deleted_assets": "",
"unable_to_remove_library": "", "unable_to_remove_library": "",
"unable_to_remove_offline_files": "",
"unable_to_remove_partner": "", "unable_to_remove_partner": "",
"unable_to_remove_reaction": "", "unable_to_remove_reaction": "",
"unable_to_repair_items": "", "unable_to_repair_items": "",
@@ -895,10 +895,10 @@
"refreshed": "Опреснено", "refreshed": "Опреснено",
"refreshes_every_file": "", "refreshes_every_file": "",
"remove": "Премахни", "remove": "Премахни",
"remove_deleted_assets": "",
"remove_from_album": "", "remove_from_album": "",
"remove_from_favorites": "", "remove_from_favorites": "",
"remove_from_shared_link": "", "remove_from_shared_link": "",
"remove_offline_files": "",
"removed_api_key": "", "removed_api_key": "",
"rename": "Преименувай", "rename": "Преименувай",
"repair": "Поправи", "repair": "Поправи",
+3 -3
View File
@@ -172,7 +172,7 @@
"paths_validated_successfully": "", "paths_validated_successfully": "",
"quota_size_gib": "", "quota_size_gib": "",
"refreshing_all_libraries": "", "refreshing_all_libraries": "",
"removing_deleted_files": "", "removing_offline_files": "",
"repair_all": "", "repair_all": "",
"repair_matched_items": "", "repair_matched_items": "",
"repaired_items": "", "repaired_items": "",
@@ -485,8 +485,8 @@
"unable_to_refresh_user": "", "unable_to_refresh_user": "",
"unable_to_remove_album_users": "", "unable_to_remove_album_users": "",
"unable_to_remove_api_key": "", "unable_to_remove_api_key": "",
"unable_to_remove_deleted_assets": "",
"unable_to_remove_library": "", "unable_to_remove_library": "",
"unable_to_remove_offline_files": "",
"unable_to_remove_partner": "", "unable_to_remove_partner": "",
"unable_to_remove_reaction": "", "unable_to_remove_reaction": "",
"unable_to_repair_items": "", "unable_to_repair_items": "",
@@ -718,10 +718,10 @@
"refreshed": "", "refreshed": "",
"refreshes_every_file": "", "refreshes_every_file": "",
"remove": "", "remove": "",
"remove_deleted_assets": "",
"remove_from_album": "", "remove_from_album": "",
"remove_from_favorites": "", "remove_from_favorites": "",
"remove_from_shared_link": "", "remove_from_shared_link": "",
"remove_offline_files": "",
"removed_api_key": "", "removed_api_key": "",
"rename": "", "rename": "",
"repair": "", "repair": "",
+7 -14
View File
@@ -8,7 +8,7 @@
"active": "Actiu", "active": "Actiu",
"activity": "Activitat", "activity": "Activitat",
"activity_changed": "L'activitat està {enabled, select, true {activada} other {desactivada}}", "activity_changed": "L'activitat està {enabled, select, true {activada} other {desactivada}}",
"add": "Afegir", "add": "Afig",
"add_a_description": "Afegiu una descripció", "add_a_description": "Afegiu una descripció",
"add_a_location": "Afegiu una ubicació", "add_a_location": "Afegiu una ubicació",
"add_a_name": "Afegir un nom", "add_a_name": "Afegir un nom",
@@ -41,7 +41,6 @@
"confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota", "confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota",
"confirm_reprocess_all_faces": "Esteu segur que voleu reprocessar totes les cares? Això també esborrarà la gent que heu anomenat.", "confirm_reprocess_all_faces": "Esteu segur que voleu reprocessar totes les cares? Això també esborrarà la gent que heu anomenat.",
"confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?", "confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?",
"create_job": "Crear tasca",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "Deshabiliteu l'inici de sessió", "disable_login": "Deshabiliteu l'inici de sessió",
"disabled": "Deshabilitat", "disabled": "Deshabilitat",
@@ -71,7 +70,6 @@
"image_thumbnail_resolution": "Resolució de la miniatura", "image_thumbnail_resolution": "Resolució de la miniatura",
"image_thumbnail_resolution_description": "S'empra per a veure grups de fotos (cronologia, vista d'àlbum, etc.). L'alta resolució pot preservar més detalls però triguen més en codificar-se, tenen fitxers més pesats i poden reduir la reactivitat de l'aplicació.", "image_thumbnail_resolution_description": "S'empra per a veure grups de fotos (cronologia, vista d'àlbum, etc.). L'alta resolució pot preservar més detalls però triguen més en codificar-se, tenen fitxers més pesats i poden reduir la reactivitat de l'aplicació.",
"job_concurrency": "{job} concurrència", "job_concurrency": "{job} concurrència",
"job_created": "Tasca creada",
"job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.", "job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.",
"job_settings": "Configuració de les tasques", "job_settings": "Configuració de les tasques",
"job_settings_description": "Gestiona la concurrència de tasques", "job_settings_description": "Gestiona la concurrència de tasques",
@@ -200,12 +198,11 @@
"password_settings": "Inici de sessió amb contrasenya", "password_settings": "Inici de sessió amb contrasenya",
"password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya", "password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya",
"paths_validated_successfully": "Tots els camins han estat validats amb èxit", "paths_validated_successfully": "Tots els camins han estat validats amb èxit",
"person_cleanup_job": "Neteja de persona",
"quota_size_gib": "Tamany de la quota (GiB)", "quota_size_gib": "Tamany de la quota (GiB)",
"refreshing_all_libraries": "Actualitzant totes les biblioteques", "refreshing_all_libraries": "Actualitzant totes les biblioteques",
"registration": "Registre d'administrador", "registration": "Registre d'administrador",
"registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.", "registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.",
"removing_deleted_files": "Eliminant fitxers fora de línia", "removing_offline_files": "Eliminant fitxers fora de línia",
"repair_all": "Reparar tot", "repair_all": "Reparar tot",
"repair_matched_items": "Coincidència {count, plural, one {# element} other {# elements}}", "repair_matched_items": "Coincidència {count, plural, one {# element} other {# elements}}",
"repaired_items": "Corregit {count, plural, one {# element} other {# elements}}", "repaired_items": "Corregit {count, plural, one {# element} other {# elements}}",
@@ -214,7 +211,6 @@
"reset_settings_to_recent_saved": "Restablir la configuració guardada més recent", "reset_settings_to_recent_saved": "Restablir la configuració guardada més recent",
"scanning_library_for_changed_files": "Escanejant llibreria per trobar fitxers modificats", "scanning_library_for_changed_files": "Escanejant llibreria per trobar fitxers modificats",
"scanning_library_for_new_files": "Escanejant llibreria per trobar fitxers nous", "scanning_library_for_new_files": "Escanejant llibreria per trobar fitxers nous",
"search_jobs": "Tasques de cerca...",
"send_welcome_email": "Enviar correu electrònic de benvinguda", "send_welcome_email": "Enviar correu electrònic de benvinguda",
"server_external_domain_settings": "Domini extern", "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 per enllaços públics compartits, incloent http(s)://",
@@ -242,7 +238,6 @@
"storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats", "storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats",
"storage_template_user_label": "<code>{label}</code> és l'etiqueta d'emmagatzematge de l'usuari", "storage_template_user_label": "<code>{label}</code> és l'etiqueta d'emmagatzematge de l'usuari",
"system_settings": "Configuració del sistema", "system_settings": "Configuració del sistema",
"tag_cleanup_job": "Neteja d'etiqueta",
"theme_custom_css_settings": "CSS personalitzat", "theme_custom_css_settings": "CSS personalitzat",
"theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.", "theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.",
"theme_settings": "Configuració del tema", "theme_settings": "Configuració del tema",
@@ -317,7 +312,6 @@
"trash_settings_description": "Gestiona la configuració de la paperera", "trash_settings_description": "Gestiona la configuració de la paperera",
"untracked_files": "Fitxers sense seguiment", "untracked_files": "Fitxers sense seguiment",
"untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error", "untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error",
"user_cleanup_job": "Neteja d'usuari",
"user_delete_delay": "El compte i els recursos de <b>{user}</b> es programaran per a la supressió permanent en {delay, plural, one {# dia} other {# dies}}.", "user_delete_delay": "El compte i els recursos de <b>{user}</b> es programaran per a la supressió permanent en {delay, plural, one {# dia} other {# dies}}.",
"user_delete_delay_settings": "Retard de la supressió", "user_delete_delay_settings": "Retard de la supressió",
"user_delete_delay_settings_description": "Nombre de dies després de la supressió per eliminar permanentment el compte i els elements d'un usuari. El treball de supressió d'usuaris s'executa a mitjanit per comprovar si hi ha usuaris preparats per eliminar. Els canvis en aquesta configuració s'avaluaran en la propera execució.", "user_delete_delay_settings_description": "Nombre de dies després de la supressió per eliminar permanentment el compte i els elements d'un usuari. El treball de supressió d'usuaris s'executa a mitjanit per comprovar si hi ha usuaris preparats per eliminar. Els canvis en aquesta configuració s'avaluaran en la propera execució.",
@@ -683,8 +677,8 @@
"unable_to_remove_api_key": "No es pot eliminar la clau de l'API", "unable_to_remove_api_key": "No es pot eliminar la clau de l'API",
"unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit", "unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "No es poden eliminar els fitxers fora de línia",
"unable_to_remove_library": "No es pot eliminar la biblioteca", "unable_to_remove_library": "No es pot eliminar la biblioteca",
"unable_to_remove_offline_files": "No es poden eliminar els fitxers fora de línia",
"unable_to_remove_partner": "No es pot eliminar company/a", "unable_to_remove_partner": "No es pot eliminar company/a",
"unable_to_remove_reaction": "No es pot eliminar la reacció", "unable_to_remove_reaction": "No es pot eliminar la reacció",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -931,7 +925,7 @@
"offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.", "offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.",
"ok": "D'acord", "ok": "D'acord",
"oldest_first": "El més vell primer", "oldest_first": "El més vell primer",
"onboarding": "Incorporació", "onboarding": "Onboarding",
"onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.", "onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.",
"onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.", "onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.",
"onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.", "onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.",
@@ -1071,10 +1065,10 @@
"remove_assets_shared_link_confirmation": "Esteu segur que voleu eliminar {count, plural, one {# recurs} other {# recursos}} d'aquest enllaç compartit?", "remove_assets_shared_link_confirmation": "Esteu segur que voleu eliminar {count, plural, one {# recurs} other {# recursos}} d'aquest enllaç compartit?",
"remove_assets_title": "Eliminar els elements?", "remove_assets_title": "Eliminar els elements?",
"remove_custom_date_range": "Elimina l'interval de dates personalitzat", "remove_custom_date_range": "Elimina l'interval de dates personalitzat",
"remove_deleted_assets": "Suprimeix fitxers fora de línia",
"remove_from_album": "Treu de l'àlbum", "remove_from_album": "Treu de l'àlbum",
"remove_from_favorites": "Eliminar dels preferits", "remove_from_favorites": "Eliminar dels preferits",
"remove_from_shared_link": "Eliminar de l'enllaç compartit", "remove_from_shared_link": "Eliminar de l'enllaç compartit",
"remove_offline_files": "Suprimeix fitxers fora de línia",
"remove_user": "Eliminar l'usuari", "remove_user": "Eliminar l'usuari",
"removed_api_key": "Eliminada la clau d'API: {name}", "removed_api_key": "Eliminada la clau d'API: {name}",
"removed_from_archive": "Eliminat de l'arxiu", "removed_from_archive": "Eliminat de l'arxiu",
@@ -1119,7 +1113,7 @@
"search_albums": "Buscar àlbums", "search_albums": "Buscar àlbums",
"search_by_context": "Buscar per context", "search_by_context": "Buscar per context",
"search_by_filename": "Cerca per nom de fitxer o extensió", "search_by_filename": "Cerca per nom de fitxer o extensió",
"search_by_filename_example": "per exemple IMG_1234.JPG o PNG", "search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
"search_camera_make": "Buscar per fabricant de càmara...", "search_camera_make": "Buscar per fabricant de càmara...",
"search_camera_model": "Buscar per model de càmera...", "search_camera_model": "Buscar per model de càmera...",
"search_city": "Buscar per ciutat...", "search_city": "Buscar per ciutat...",
@@ -1130,7 +1124,6 @@
"search_options": "Opcions de cerca", "search_options": "Opcions de cerca",
"search_people": "Buscar persones", "search_people": "Buscar persones",
"search_places": "Buscar llocs", "search_places": "Buscar llocs",
"search_settings": "Configuració de cerca",
"search_state": "Buscar per regió...", "search_state": "Buscar per regió...",
"search_tags": "Cercant etiquetes...", "search_tags": "Cercant etiquetes...",
"search_timezone": "Buscar per fus horari...", "search_timezone": "Buscar per fus horari...",
@@ -1247,7 +1240,7 @@
"tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques", "tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques",
"tag_not_found_question": "No trobeu una etiqueta? Creeu-ne una <link>aquí</link>", "tag_not_found_question": "No trobeu una etiqueta? Creeu-ne una <link>aquí</link>",
"tag_updated": "Etiqueta actualizada: {tag}", "tag_updated": "Etiqueta actualizada: {tag}",
"tagged_assets": "{count, plural, one {#Etiquetat} other {#Etiquetats}} {count, plural, one {# actiu} other {# actius}}", "tagged_assets": "{count, plural, one {#Etiquetat} other {#Etiquetats}} {count, plural, one {# actiu} other {# actius}}",
"tags": "Etiquetes", "tags": "Etiquetes",
"template": "Plantilla", "template": "Plantilla",
"theme": "Tema", "theme": "Tema",
+3 -10
View File
@@ -41,7 +41,6 @@
"confirm_email_below": "Pro potvrzení zadejte níže \"{email}\"", "confirm_email_below": "Pro potvrzení zadejte níže \"{email}\"",
"confirm_reprocess_all_faces": "Opravdu chcete znovu zpracovat všechny obličeje? Tím se vymažou i pojmenované osoby.", "confirm_reprocess_all_faces": "Opravdu chcete znovu zpracovat všechny obličeje? Tím se vymažou i pojmenované osoby.",
"confirm_user_password_reset": "Opravdu chcete obnovit heslo uživatele {user}?", "confirm_user_password_reset": "Opravdu chcete obnovit heslo uživatele {user}?",
"create_job": "Vytvořit úlohu",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "Zakázat přihlášení", "disable_login": "Zakázat přihlášení",
"disabled": "Zakázáno", "disabled": "Zakázáno",
@@ -71,7 +70,6 @@
"image_thumbnail_resolution": "Rozlišení miniatur", "image_thumbnail_resolution": "Rozlišení miniatur",
"image_thumbnail_resolution_description": "Používá se při prohlížení skupin fotografií (hlavní časová osa, zobrazení alba atd.). Vyšší rozlišení může zachovat více detailů, ale trvá déle, než se zakóduje, má větší velikost souboru a může snížit odezvu aplikace.", "image_thumbnail_resolution_description": "Používá se při prohlížení skupin fotografií (hlavní časová osa, zobrazení alba atd.). Vyšší rozlišení může zachovat více detailů, ale trvá déle, než se zakóduje, má větší velikost souboru a může snížit odezvu aplikace.",
"job_concurrency": "Souběžnost {job}", "job_concurrency": "Souběžnost {job}",
"job_created": "Úloha vytvořena",
"job_not_concurrency_safe": "Tato úloha není bezpečená pro souběh.", "job_not_concurrency_safe": "Tato úloha není bezpečená pro souběh.",
"job_settings": "Úlohy", "job_settings": "Úlohy",
"job_settings_description": "Správa souběžnosti úloh", "job_settings_description": "Správa souběžnosti úloh",
@@ -200,12 +198,11 @@
"password_settings": "Přihlášení heslem", "password_settings": "Přihlášení heslem",
"password_settings_description": "Správa nastavení přihlašování pomocí hesla", "password_settings_description": "Správa nastavení přihlašování pomocí hesla",
"paths_validated_successfully": "Všechny cesty byly úspěšně ověřeny", "paths_validated_successfully": "Všechny cesty byly úspěšně ověřeny",
"person_cleanup_job": "Promazání osob",
"quota_size_gib": "Velikost kvóty (GiB)", "quota_size_gib": "Velikost kvóty (GiB)",
"refreshing_all_libraries": "Obnovení všech knihoven", "refreshing_all_libraries": "Obnovení všech knihoven",
"registration": "Registrace správce", "registration": "Registrace správce",
"registration_description": "Vzhledem k tomu, že jste prvním uživatelem v systému, budete přiřazen jako správce a budete zodpovědný za úkoly správy a další uživatelé budou vytvořeni vámi.", "registration_description": "Vzhledem k tomu, že jste prvním uživatelem v systému, budete přiřazen jako správce a budete zodpovědný za úkoly správy a další uživatelé budou vytvořeni vámi.",
"removing_deleted_files": "Odstranění offline souborů", "removing_offline_files": "Odstranění offline souborů",
"repair_all": "Opravit vše", "repair_all": "Opravit vše",
"repair_matched_items": "Shoda {count, plural, one {# položky} other {# položek}}", "repair_matched_items": "Shoda {count, plural, one {# položky} other {# položek}}",
"repaired_items": "{count, plural, one {Opravena # položka} few {Opraveny # položky} other {Opraveno # položek}}", "repaired_items": "{count, plural, one {Opravena # položka} few {Opraveny # položky} other {Opraveno # položek}}",
@@ -214,7 +211,6 @@
"reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení", "reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení",
"scanning_library_for_changed_files": "Hledání změněných souborů v knihovně", "scanning_library_for_changed_files": "Hledání změněných souborů v knihovně",
"scanning_library_for_new_files": "Hledání nových souborů v knihovně", "scanning_library_for_new_files": "Hledání nových souborů v knihovně",
"search_jobs": "Hledat úlohy...",
"send_welcome_email": "Odeslat uvítací e-mail", "send_welcome_email": "Odeslat uvítací e-mail",
"server_external_domain_settings": "Externí doména", "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 pro veřejně sdílené odkazy, včetně http(s)://",
@@ -242,7 +238,6 @@
"storage_template_settings_description": "Správa struktury složek a názvů nahraných souborů", "storage_template_settings_description": "Správa struktury složek a názvů nahraných souborů",
"storage_template_user_label": "<code>{label}</code> je štítek úložiště uživatele", "storage_template_user_label": "<code>{label}</code> je štítek úložiště uživatele",
"system_settings": "Systémová nastavení", "system_settings": "Systémová nastavení",
"tag_cleanup_job": "Promazání značek",
"theme_custom_css_settings": "Vlastní CSS", "theme_custom_css_settings": "Vlastní CSS",
"theme_custom_css_settings_description": "Kaskádové styly umožňují přizpůsobit design aplikace Immich.", "theme_custom_css_settings_description": "Kaskádové styly umožňují přizpůsobit design aplikace Immich.",
"theme_settings": "Motivy", "theme_settings": "Motivy",
@@ -317,7 +312,6 @@
"trash_settings_description": "Správa nastavení koše", "trash_settings_description": "Správa nastavení koše",
"untracked_files": "Neznámé soubory", "untracked_files": "Neznámé soubory",
"untracked_files_description": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě", "untracked_files_description": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě",
"user_cleanup_job": "Promazání uživatelů",
"user_delete_delay": "Účet a položky uživatele <b>{user}</b> budou trvale smazány za {delay, plural, one {# den} few {# dny} other {# dní}}.", "user_delete_delay": "Účet a položky uživatele <b>{user}</b> budou trvale smazány za {delay, plural, one {# den} few {# dny} other {# dní}}.",
"user_delete_delay_settings": "Odložení odstranění", "user_delete_delay_settings": "Odložení odstranění",
"user_delete_delay_settings_description": "Počet dní po odstranění, po kterých bude odstraněn účet a položky uživatele. Úloha odstraňování uživatelů se spouští o půlnoci a kontroluje uživatele, kteří jsou připraveni k odstranění. Změny tohoto nastavení se vyhodnotí při dalším spuštění.", "user_delete_delay_settings_description": "Počet dní po odstranění, po kterých bude odstraněn účet a položky uživatele. Úloha odstraňování uživatelů se spouští o půlnoci a kontroluje uživatele, kteří jsou připraveni k odstranění. Změny tohoto nastavení se vyhodnotí při dalším spuštění.",
@@ -684,8 +678,8 @@
"unable_to_remove_api_key": "Nelze odstranit API klíč", "unable_to_remove_api_key": "Nelze odstranit API klíč",
"unable_to_remove_assets_from_shared_link": "Nelze odstranit položky ze sdíleného odkazu", "unable_to_remove_assets_from_shared_link": "Nelze odstranit položky ze sdíleného odkazu",
"unable_to_remove_comment": "Nelze odstranit komentář", "unable_to_remove_comment": "Nelze odstranit komentář",
"unable_to_remove_deleted_assets": "Nelze odstranit offline soubory",
"unable_to_remove_library": "Nelze odstranit knihovnu", "unable_to_remove_library": "Nelze odstranit knihovnu",
"unable_to_remove_offline_files": "Nelze odstranit offline soubory",
"unable_to_remove_partner": "Nelze odebrat partnera", "unable_to_remove_partner": "Nelze odebrat partnera",
"unable_to_remove_reaction": "Nelze odstranit reakci", "unable_to_remove_reaction": "Nelze odstranit reakci",
"unable_to_remove_user": "Nelze odebrat uživatele", "unable_to_remove_user": "Nelze odebrat uživatele",
@@ -1089,10 +1083,10 @@
"remove_assets_shared_link_confirmation": "Opravdu chcete ze sdíleného odkazu odstranit {count, plural, one {# položku} few {# položky} other {# položek}}?", "remove_assets_shared_link_confirmation": "Opravdu chcete ze sdíleného odkazu odstranit {count, plural, one {# položku} few {# položky} other {# položek}}?",
"remove_assets_title": "Odstranit položky?", "remove_assets_title": "Odstranit položky?",
"remove_custom_date_range": "Odstranit vlastní rozsah datumů", "remove_custom_date_range": "Odstranit vlastní rozsah datumů",
"remove_deleted_assets": "Odstranit offline soubory",
"remove_from_album": "Odstranit z alba", "remove_from_album": "Odstranit z alba",
"remove_from_favorites": "Odstranit z oblíbených", "remove_from_favorites": "Odstranit z oblíbených",
"remove_from_shared_link": "Odstranit ze sdíleného odkazu", "remove_from_shared_link": "Odstranit ze sdíleného odkazu",
"remove_offline_files": "Odstranit offline soubory",
"remove_user": "Odebrat uživatele", "remove_user": "Odebrat uživatele",
"removed_api_key": "Odstraněn API klíč: {name}", "removed_api_key": "Odstraněn API klíč: {name}",
"removed_from_archive": "Odstraněno z archivu", "removed_from_archive": "Odstraněno z archivu",
@@ -1148,7 +1142,6 @@
"search_options": "Možnosti vyhledávání", "search_options": "Možnosti vyhledávání",
"search_people": "Vyhledat lidi", "search_people": "Vyhledat lidi",
"search_places": "Vyhledat místa", "search_places": "Vyhledat místa",
"search_settings": "Hledat nastavení",
"search_state": "Vyhledat stát...", "search_state": "Vyhledat stát...",
"search_tags": "Vyhledávat značky...", "search_tags": "Vyhledávat značky...",
"search_timezone": "Vyhledat časové pásmo...", "search_timezone": "Vyhledat časové pásmo...",
+3 -3
View File
@@ -202,7 +202,7 @@
"refreshing_all_libraries": "Opdaterer alle biblioteker", "refreshing_all_libraries": "Opdaterer alle biblioteker",
"registration": "Administratorregistrering", "registration": "Administratorregistrering",
"registration_description": "Da du er den første bruger i systemet, får du tildelt rollen som administrator og ansvar for administration og oprettelsen af nye brugere.", "registration_description": "Da du er den første bruger i systemet, får du tildelt rollen som administrator og ansvar for administration og oprettelsen af nye brugere.",
"removing_deleted_files": "Fjerner offline-filer", "removing_offline_files": "Fjerner offline-filer",
"repair_all": "Reparér alle", "repair_all": "Reparér alle",
"repair_matched_items": "Har parret {count, plural, one {# element} other {# elementer}}", "repair_matched_items": "Har parret {count, plural, one {# element} other {# elementer}}",
"repaired_items": "Reparerede {count, plural, one {# element} other {# elementer}}", "repaired_items": "Reparerede {count, plural, one {# element} other {# elementer}}",
@@ -563,8 +563,8 @@
"unable_to_remove_album_users": "Ikke i stand til at fjerne brugere fra album", "unable_to_remove_album_users": "Ikke i stand til at fjerne brugere fra album",
"unable_to_remove_api_key": "Kunne ikke fjerne API-nøgle", "unable_to_remove_api_key": "Kunne ikke fjerne API-nøgle",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "Kunne ikke fjerne offlinefiler",
"unable_to_remove_library": "Ikke i stand til at fjerne bibliotek", "unable_to_remove_library": "Ikke i stand til at fjerne bibliotek",
"unable_to_remove_offline_files": "Kunne ikke fjerne offlinefiler",
"unable_to_remove_partner": "Ikke i stand til at fjerne partner", "unable_to_remove_partner": "Ikke i stand til at fjerne partner",
"unable_to_remove_reaction": "Ikke i stand til at reaktion", "unable_to_remove_reaction": "Ikke i stand til at reaktion",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -811,10 +811,10 @@
"refreshed": "Opdateret", "refreshed": "Opdateret",
"refreshes_every_file": "Opdaterer alle filer", "refreshes_every_file": "Opdaterer alle filer",
"remove": "Fjern", "remove": "Fjern",
"remove_deleted_assets": "Fjern fra offlinefiler",
"remove_from_album": "Fjern fra album", "remove_from_album": "Fjern fra album",
"remove_from_favorites": "Fjern fra favoritter", "remove_from_favorites": "Fjern fra favoritter",
"remove_from_shared_link": "Fjern fra delt link", "remove_from_shared_link": "Fjern fra delt link",
"remove_offline_files": "Fjern fra offlinefiler",
"removed_api_key": "Fjernede API-nøgle: {name}", "removed_api_key": "Fjernede API-nøgle: {name}",
"rename": "Omdøb", "rename": "Omdøb",
"repair": "Reparér", "repair": "Reparér",
+3 -10
View File
@@ -41,7 +41,6 @@
"confirm_email_below": "Bestätige, indem du \"{email}\" unten eingibst", "confirm_email_below": "Bestätige, indem du \"{email}\" unten eingibst",
"confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.", "confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.",
"confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?", "confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?",
"create_job": "Job erstellen",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "Login deaktvieren", "disable_login": "Login deaktvieren",
"disabled": "Deaktiviert", "disabled": "Deaktiviert",
@@ -71,7 +70,6 @@
"image_thumbnail_resolution": "Miniaturansichts-Auflösung", "image_thumbnail_resolution": "Miniaturansichts-Auflösung",
"image_thumbnail_resolution_description": "Dies wird bei der Anzeige von Bildergruppen („Zeitleiste“, „Albumansicht“ usw.) verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.", "image_thumbnail_resolution_description": "Dies wird bei der Anzeige von Bildergruppen („Zeitleiste“, „Albumansicht“ usw.) verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.",
"job_concurrency": "{job} - (Anzahl gleichzeitiger Prozesse)", "job_concurrency": "{job} - (Anzahl gleichzeitiger Prozesse)",
"job_created": "Job erstellt",
"job_not_concurrency_safe": "Dieser Job ist nicht parallelisierungssicher.", "job_not_concurrency_safe": "Dieser Job ist nicht parallelisierungssicher.",
"job_settings": "Job-Einstellungen", "job_settings": "Job-Einstellungen",
"job_settings_description": "Gleichzeitige Job-Prozessen verwalten", "job_settings_description": "Gleichzeitige Job-Prozessen verwalten",
@@ -200,12 +198,11 @@
"password_settings": "Passwort Login", "password_settings": "Passwort Login",
"password_settings_description": "Passwort-Anmeldeeinstellungen verwalten", "password_settings_description": "Passwort-Anmeldeeinstellungen verwalten",
"paths_validated_successfully": "Alle Pfade wurden erfolgreich validiert", "paths_validated_successfully": "Alle Pfade wurden erfolgreich validiert",
"person_cleanup_job": "Personen aufräumen",
"quota_size_gib": "Kontingent (GiB)", "quota_size_gib": "Kontingent (GiB)",
"refreshing_all_libraries": "Alle Bibliotheken aktualisieren", "refreshing_all_libraries": "Alle Bibliotheken aktualisieren",
"registration": "Admin-Registrierung", "registration": "Admin-Registrierung",
"registration_description": "Da du der erste Benutzer im System bist, wirst du als Admin zugewiesen und bist für administrative Aufgaben zuständig. Weitere Benutzer werden von dir erstellt.", "registration_description": "Da du der erste Benutzer im System bist, wirst du als Admin zugewiesen und bist für administrative Aufgaben zuständig. Weitere Benutzer werden von dir erstellt.",
"removing_deleted_files": "Offline-Dateien entfernen", "removing_offline_files": "Offline-Dateien entfernen",
"repair_all": "Alle reparieren", "repair_all": "Alle reparieren",
"repair_matched_items": "{count, plural, one {# Eintrag} other {# Einträge}} gefunden", "repair_matched_items": "{count, plural, one {# Eintrag} other {# Einträge}} gefunden",
"repaired_items": "{count, plural, one {# Eintrag} other {# Einträge}} repariert", "repaired_items": "{count, plural, one {# Eintrag} other {# Einträge}} repariert",
@@ -214,7 +211,6 @@
"reset_settings_to_recent_saved": "Einstellungen auf die zuletzt gespeicherten Einstellungen zurücksetzen", "reset_settings_to_recent_saved": "Einstellungen auf die zuletzt gespeicherten Einstellungen zurücksetzen",
"scanning_library_for_changed_files": "Untersuche Bibliothek auf geänderte Dateien", "scanning_library_for_changed_files": "Untersuche Bibliothek auf geänderte Dateien",
"scanning_library_for_new_files": "Untersuche Bibliothek auf neue Dateien", "scanning_library_for_new_files": "Untersuche Bibliothek auf neue Dateien",
"search_jobs": "Jobs suchen...",
"send_welcome_email": "Begrüssungsmail senden", "send_welcome_email": "Begrüssungsmail senden",
"server_external_domain_settings": "Externe Domain", "server_external_domain_settings": "Externe Domain",
"server_external_domain_settings_description": "Domäne für öffentlich freigegebene Links, einschließlich http(s)://", "server_external_domain_settings_description": "Domäne für öffentlich freigegebene Links, einschließlich http(s)://",
@@ -242,7 +238,6 @@
"storage_template_settings_description": "Die Ordnerstruktur und den Dateinamen der hochgeladenen Datei verwalten", "storage_template_settings_description": "Die Ordnerstruktur und den Dateinamen der hochgeladenen Datei verwalten",
"storage_template_user_label": "<code>{label}</code> is das Speicher-Label des Benutzers", "storage_template_user_label": "<code>{label}</code> is das Speicher-Label des Benutzers",
"system_settings": "Systemeinstellungen", "system_settings": "Systemeinstellungen",
"tag_cleanup_job": "Tags aufräumen",
"theme_custom_css_settings": "Benutzerdefiniertes CSS", "theme_custom_css_settings": "Benutzerdefiniertes CSS",
"theme_custom_css_settings_description": "Mit Cascading Style Sheets (CSS) kann das Design von Immich angepasst werden.", "theme_custom_css_settings_description": "Mit Cascading Style Sheets (CSS) kann das Design von Immich angepasst werden.",
"theme_settings": "Theme-Einstellungen", "theme_settings": "Theme-Einstellungen",
@@ -317,7 +312,6 @@
"trash_settings_description": "Papierkorb-Einstellungen verwalten", "trash_settings_description": "Papierkorb-Einstellungen verwalten",
"untracked_files": "Unverfolgte Dateien", "untracked_files": "Unverfolgte Dateien",
"untracked_files_description": "Diese Dateien werden nicht von der Application getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein", "untracked_files_description": "Diese Dateien werden nicht von der Application getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein",
"user_cleanup_job": "Benutzer aufräumen",
"user_delete_delay": "Das Konto und die Dateien von <b>{user}</b> werden in {delay, plural, one {einem Tag} other {# Tagen}} für eine permanente Löschung geplant.", "user_delete_delay": "Das Konto und die Dateien von <b>{user}</b> werden in {delay, plural, one {einem Tag} other {# Tagen}} für eine permanente Löschung geplant.",
"user_delete_delay_settings": "Verzögerung für das Löschen von Benutzern", "user_delete_delay_settings": "Verzögerung für das Löschen von Benutzern",
"user_delete_delay_settings_description": "Gibt die Anzahl der Tage bis zur endgültigen Löschung eines Kontos und seiner Dateien an. Der Benutzerlöschauftrag wird täglich um Mitternacht ausgeführt, um zu überprüfen, ob Nutzer zur Löschung bereit sind. Änderungen an dieser Einstellung werden erst bei der nächsten Ausführung berücksichtigt.", "user_delete_delay_settings_description": "Gibt die Anzahl der Tage bis zur endgültigen Löschung eines Kontos und seiner Dateien an. Der Benutzerlöschauftrag wird täglich um Mitternacht ausgeführt, um zu überprüfen, ob Nutzer zur Löschung bereit sind. Änderungen an dieser Einstellung werden erst bei der nächsten Ausführung berücksichtigt.",
@@ -684,8 +678,8 @@
"unable_to_remove_api_key": "API-Schlüssel konnte nicht entfernt werden", "unable_to_remove_api_key": "API-Schlüssel konnte nicht entfernt werden",
"unable_to_remove_assets_from_shared_link": "Dateien konnten nicht von geteiltem Link entfernt werden", "unable_to_remove_assets_from_shared_link": "Dateien konnten nicht von geteiltem Link entfernt werden",
"unable_to_remove_comment": "Kommentar kann nicht entfernt werden", "unable_to_remove_comment": "Kommentar kann nicht entfernt werden",
"unable_to_remove_deleted_assets": "Offline-Dateien konnten nicht entfernt werden",
"unable_to_remove_library": "Bibliothek kann nicht entfernt werden", "unable_to_remove_library": "Bibliothek kann nicht entfernt werden",
"unable_to_remove_offline_files": "Offline-Dateien konnten nicht entfernt werden",
"unable_to_remove_partner": "Partner kann nicht entfernt werden", "unable_to_remove_partner": "Partner kann nicht entfernt werden",
"unable_to_remove_reaction": "Reaktion kann nicht entfernt werden", "unable_to_remove_reaction": "Reaktion kann nicht entfernt werden",
"unable_to_remove_user": "Benutzer kann nicht entfernt werden", "unable_to_remove_user": "Benutzer kann nicht entfernt werden",
@@ -1088,10 +1082,10 @@
"remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?", "remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?",
"remove_assets_title": "Dateien entfernen?", "remove_assets_title": "Dateien entfernen?",
"remove_custom_date_range": "Benutzerdefinierten Datumsbereich entfernen", "remove_custom_date_range": "Benutzerdefinierten Datumsbereich entfernen",
"remove_deleted_assets": "Offline-Dateien entfernen",
"remove_from_album": "Aus Album entfernen", "remove_from_album": "Aus Album entfernen",
"remove_from_favorites": "Aus Favoriten entfernen", "remove_from_favorites": "Aus Favoriten entfernen",
"remove_from_shared_link": "Aus geteilten Link entfernen", "remove_from_shared_link": "Aus geteilten Link entfernen",
"remove_offline_files": "Offline-Dateien entfernen",
"remove_user": "Nutzer entfernen", "remove_user": "Nutzer entfernen",
"removed_api_key": "API-Schlüssel {name} wurde entfernt", "removed_api_key": "API-Schlüssel {name} wurde entfernt",
"removed_from_archive": "Aus dem Archiv entfernt", "removed_from_archive": "Aus dem Archiv entfernt",
@@ -1147,7 +1141,6 @@
"search_options": "Suchoptionen", "search_options": "Suchoptionen",
"search_people": "Suche nach Personen", "search_people": "Suche nach Personen",
"search_places": "Suche nach Orten", "search_places": "Suche nach Orten",
"search_settings": "Suche nach Einstellungen",
"search_state": "Suche nach Bundesland / Provinz...", "search_state": "Suche nach Bundesland / Provinz...",
"search_tags": "Sache nach Tags...", "search_tags": "Sache nach Tags...",
"search_timezone": "Suche nach Zeitzone...", "search_timezone": "Suche nach Zeitzone...",
+21 -12
View File
@@ -28,7 +28,6 @@
"added_to_favorites_count": "Added {count, number} to favorites", "added_to_favorites_count": "Added {count, number} to favorites",
"admin": { "admin": {
"add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".", "add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".",
"asset_offline_description": "This external library asset is no longer found on disk and has been moved to trash. If the file was moved within the library, check your timeline for the new corresponding asset. To restore this asset, please ensure that the file path below can be accessed by Immich and scan the library.",
"authentication_settings": "Authentication Settings", "authentication_settings": "Authentication Settings",
"authentication_settings_description": "Manage password, OAuth, and other authentication settings", "authentication_settings_description": "Manage password, OAuth, and other authentication settings",
"authentication_settings_disable_all": "Are you sure you want to disable all login methods? Login will be completely disabled.", "authentication_settings_disable_all": "Are you sure you want to disable all login methods? Login will be completely disabled.",
@@ -117,6 +116,12 @@
"machine_learning_min_detection_score_description": "Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives.", "machine_learning_min_detection_score_description": "Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives.",
"machine_learning_min_recognized_faces": "Minimum recognized faces", "machine_learning_min_recognized_faces": "Minimum recognized faces",
"machine_learning_min_recognized_faces_description": "The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person.", "machine_learning_min_recognized_faces_description": "The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person.",
"machine_learning_preload_model": "Preload model",
"machine_learning_preload_model_enabled": "Enable preload model",
"machine_learning_preload_model_enabled_description": "Preload the textual model during the connexion instead of during the first search",
"machine_learning_preload_model_setting_description": "Preload the textual model during the connexion",
"machine_learning_preload_model_ttl": "Inactivity time before a model in unloaded",
"machine_learning_preload_model_ttl_description": "Preload the textual model during the connexion",
"machine_learning_settings": "Machine Learning Settings", "machine_learning_settings": "Machine Learning Settings",
"machine_learning_settings_description": "Manage machine learning features and settings", "machine_learning_settings_description": "Manage machine learning features and settings",
"machine_learning_smart_search": "Smart Search", "machine_learning_smart_search": "Smart Search",
@@ -204,13 +209,15 @@
"refreshing_all_libraries": "Refreshing all libraries", "refreshing_all_libraries": "Refreshing all libraries",
"registration": "Admin Registration", "registration": "Admin Registration",
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
"removing_offline_files": "Removing Offline Files",
"repair_all": "Repair All", "repair_all": "Repair All",
"repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}",
"repaired_items": "Repaired {count, plural, one {# item} other {# items}}", "repaired_items": "Repaired {count, plural, one {# item} other {# items}}",
"require_password_change_on_login": "Require user to change password on first login", "require_password_change_on_login": "Require user to change password on first login",
"reset_settings_to_default": "Reset settings to default", "reset_settings_to_default": "Reset settings to default",
"reset_settings_to_recent_saved": "Reset settings to the recent saved settings", "reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
"scanning_library": "Scanning library", "scanning_library_for_changed_files": "Scanning library for changed files",
"scanning_library_for_new_files": "Scanning library for new files",
"search_jobs": "Search jobs...", "search_jobs": "Search jobs...",
"send_welcome_email": "Send welcome email", "send_welcome_email": "Send welcome email",
"server_external_domain_settings": "External domain", "server_external_domain_settings": "External domain",
@@ -389,8 +396,8 @@
"asset_filename_is_offline": "Asset {filename} is offline", "asset_filename_is_offline": "Asset {filename} is offline",
"asset_has_unassigned_faces": "Asset has unassigned faces", "asset_has_unassigned_faces": "Asset has unassigned faces",
"asset_hashing": "Hashing...", "asset_hashing": "Hashing...",
"asset_offline": "Asset Offline", "asset_offline": "Asset offline",
"asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.", "asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.",
"asset_skipped": "Skipped", "asset_skipped": "Skipped",
"asset_skipped_in_trash": "In trash", "asset_skipped_in_trash": "In trash",
"asset_uploaded": "Uploaded", "asset_uploaded": "Uploaded",
@@ -403,7 +410,7 @@
"assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash", "assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash",
"assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}", "assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}",
"assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}",
"assets_restore_confirmation": "Are you sure you want to restore all your trashed assets? You cannot undo this action! Note that any offline assets cannot be restored this way.", "assets_restore_confirmation": "Are you sure you want to restore all your trashed assets? You cannot undo this action!",
"assets_restored_count": "Restored {count, plural, one {# asset} other {# assets}}", "assets_restored_count": "Restored {count, plural, one {# asset} other {# assets}}",
"assets_trashed_count": "Trashed {count, plural, one {# asset} other {# assets}}", "assets_trashed_count": "Trashed {count, plural, one {# asset} other {# assets}}",
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} already part of the album", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} already part of the album",
@@ -506,14 +513,13 @@
"delete_api_key_prompt": "Are you sure you want to delete this API key?", "delete_api_key_prompt": "Are you sure you want to delete this API key?",
"delete_duplicates_confirmation": "Are you sure you want to permanently delete these duplicates?", "delete_duplicates_confirmation": "Are you sure you want to permanently delete these duplicates?",
"delete_key": "Delete key", "delete_key": "Delete key",
"delete_library": "Delete Library", "delete_library": "Delete library",
"delete_link": "Delete link", "delete_link": "Delete link",
"delete_shared_link": "Delete shared link", "delete_shared_link": "Delete shared link",
"delete_tag": "Delete tag", "delete_tag": "Delete tag",
"delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?", "delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?",
"delete_user": "Delete user", "delete_user": "Delete user",
"deleted_shared_link": "Deleted shared link", "deleted_shared_link": "Deleted shared link",
"deletes_missing_assets": "Deletes assets missing from disk",
"description": "Description", "description": "Description",
"details": "Details", "details": "Details",
"direction": "Direction", "direction": "Direction",
@@ -663,8 +669,8 @@
"unable_to_remove_album_users": "Unable to remove users from album", "unable_to_remove_album_users": "Unable to remove users from album",
"unable_to_remove_api_key": "Unable to remove API Key", "unable_to_remove_api_key": "Unable to remove API Key",
"unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link", "unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link",
"unable_to_remove_deleted_assets": "Unable to remove offline files",
"unable_to_remove_library": "Unable to remove library", "unable_to_remove_library": "Unable to remove library",
"unable_to_remove_offline_files": "Unable to remove offline files",
"unable_to_remove_partner": "Unable to remove partner", "unable_to_remove_partner": "Unable to remove partner",
"unable_to_remove_reaction": "Unable to remove reaction", "unable_to_remove_reaction": "Unable to remove reaction",
"unable_to_repair_items": "Unable to repair items", "unable_to_repair_items": "Unable to repair items",
@@ -725,6 +731,7 @@
"fix_incorrect_match": "Fix incorrect match", "fix_incorrect_match": "Fix incorrect match",
"folders": "Folders", "folders": "Folders",
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
"force_re-scan_library_files": "Force Re-scan All Library Files",
"forward": "Forward", "forward": "Forward",
"general": "General", "general": "General",
"get_help": "Get Help", "get_help": "Get Help",
@@ -892,6 +899,7 @@
"onboarding_welcome_user": "Welcome, {user}", "onboarding_welcome_user": "Welcome, {user}",
"online": "Online", "online": "Online",
"only_favorites": "Only favorites", "only_favorites": "Only favorites",
"only_refreshes_modified_files": "Only refreshes modified files",
"open_in_map_view": "Open in map view", "open_in_map_view": "Open in map view",
"open_in_openstreetmap": "Open in OpenStreetMap", "open_in_openstreetmap": "Open in OpenStreetMap",
"open_the_search_filters": "Open the search filters", "open_the_search_filters": "Open the search filters",
@@ -1011,7 +1019,7 @@
"refresh_metadata": "Refresh metadata", "refresh_metadata": "Refresh metadata",
"refresh_thumbnails": "Refresh thumbnails", "refresh_thumbnails": "Refresh thumbnails",
"refreshed": "Refreshed", "refreshed": "Refreshed",
"refreshes_every_file": "Re-reads all existing and new files", "refreshes_every_file": "Refreshes every file",
"refreshing_encoded_video": "Refreshing encoded video", "refreshing_encoded_video": "Refreshing encoded video",
"refreshing_metadata": "Refreshing metadata", "refreshing_metadata": "Refreshing metadata",
"regenerating_thumbnails": "Regenerating thumbnails", "regenerating_thumbnails": "Regenerating thumbnails",
@@ -1020,10 +1028,10 @@
"remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?", "remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?",
"remove_assets_title": "Remove assets?", "remove_assets_title": "Remove assets?",
"remove_custom_date_range": "Remove custom date range", "remove_custom_date_range": "Remove custom date range",
"remove_deleted_assets": "Remove Deleted Assets",
"remove_from_album": "Remove from album", "remove_from_album": "Remove from album",
"remove_from_favorites": "Remove from favorites", "remove_from_favorites": "Remove from favorites",
"remove_from_shared_link": "Remove from shared link", "remove_from_shared_link": "Remove from shared link",
"remove_offline_files": "Remove Offline Files",
"remove_user": "Remove user", "remove_user": "Remove user",
"removed_api_key": "Removed API Key: {name}", "removed_api_key": "Removed API Key: {name}",
"removed_from_archive": "Removed from archive", "removed_from_archive": "Removed from archive",
@@ -1059,7 +1067,8 @@
"saved_settings": "Saved settings", "saved_settings": "Saved settings",
"say_something": "Say something", "say_something": "Say something",
"scan_all_libraries": "Scan All Libraries", "scan_all_libraries": "Scan All Libraries",
"scan_library": "Scan", "scan_all_library_files": "Re-scan All Library Files",
"scan_new_library_files": "Scan New Library Files",
"scan_settings": "Scan Settings", "scan_settings": "Scan Settings",
"scanning_for_album": "Scanning for album...", "scanning_for_album": "Scanning for album...",
"search": "Search", "search": "Search",
@@ -1191,7 +1200,7 @@
"tag_assets": "Tag assets", "tag_assets": "Tag assets",
"tag_created": "Created tag: {tag}", "tag_created": "Created tag: {tag}",
"tag_feature_description": "Browsing photos and videos grouped by logical tag topics", "tag_feature_description": "Browsing photos and videos grouped by logical tag topics",
"tag_not_found_question": "Cannot find a tag? <link>Create a new tag.</link>", "tag_not_found_question": "Cannot find a tag? Create one <link>here</link>",
"tag_updated": "Updated tag: {tag}", "tag_updated": "Updated tag: {tag}",
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}", "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
"tags": "Tags", "tags": "Tags",
+3 -10
View File
@@ -41,7 +41,6 @@
"confirm_email_below": "Para confirmar, escribe \"{email}\" debajo", "confirm_email_below": "Para confirmar, escribe \"{email}\" debajo",
"confirm_reprocess_all_faces": "¿Estás seguro de que quieres volver a procesar todas las caras? Esto también eliminará las personas a las que le hayas asignado nombre.", "confirm_reprocess_all_faces": "¿Estás seguro de que quieres volver a procesar todas las caras? Esto también eliminará las personas a las que le hayas asignado nombre.",
"confirm_user_password_reset": "¿Estás seguro de que quieres resetear la contraseña de {user}?", "confirm_user_password_reset": "¿Estás seguro de que quieres resetear la contraseña de {user}?",
"create_job": "Crear trabajo",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "Deshabilitar inicio de sesión", "disable_login": "Deshabilitar inicio de sesión",
"disabled": "Deshabilitado", "disabled": "Deshabilitado",
@@ -71,7 +70,6 @@
"image_thumbnail_resolution": "Resolución de las miniaturas", "image_thumbnail_resolution": "Resolución de las miniaturas",
"image_thumbnail_resolution_description": "Se utiliza para ver grupos de fotos (cronología, vista de álbum, etc.). Las resoluciones más altas pueden conservar más detalles, pero tardan más en codificarse, tienen archivos de mayor tamaño y pueden reducir la reactividad de la aplicación.", "image_thumbnail_resolution_description": "Se utiliza para ver grupos de fotos (cronología, vista de álbum, etc.). Las resoluciones más altas pueden conservar más detalles, pero tardan más en codificarse, tienen archivos de mayor tamaño y pueden reducir la reactividad de la aplicación.",
"job_concurrency": "{job}: Procesos simultáneos", "job_concurrency": "{job}: Procesos simultáneos",
"job_created": "Trabajo creado",
"job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.", "job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.",
"job_settings": "Configuración tareas", "job_settings": "Configuración tareas",
"job_settings_description": "Administrar tareas simultáneas", "job_settings_description": "Administrar tareas simultáneas",
@@ -200,12 +198,11 @@
"password_settings": "Contraseña de Acceso", "password_settings": "Contraseña de Acceso",
"password_settings_description": "Administrar la configuración de inicio de sesión con contraseña", "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", "paths_validated_successfully": "Todas las carpetas se han validado satisfactoriamente",
"person_cleanup_job": "Limpieza de personas",
"quota_size_gib": "Tamaño de Quota (GiB)", "quota_size_gib": "Tamaño de Quota (GiB)",
"refreshing_all_libraries": "Actualizar todas las bibliotecas", "refreshing_all_libraries": "Actualizar todas las bibliotecas",
"registration": "Registrar administrador", "registration": "Registrar administrador",
"registration_description": "Dado que eres el primer usuario del sistema, se te asignará como Admin y serás responsable de las tareas administrativas, y de crear a los usuarios adicionales.", "registration_description": "Dado que eres el primer usuario del sistema, se te asignará como Admin y serás responsable de las tareas administrativas, y de crear a los usuarios adicionales.",
"removing_deleted_files": "Eliminando archivos sin conexión", "removing_offline_files": "Eliminando archivos sin conexión",
"repair_all": "Reparar todo", "repair_all": "Reparar todo",
"repair_matched_items": "Coincidencia {count, plural, one {# elemento} other {# elementos}}", "repair_matched_items": "Coincidencia {count, plural, one {# elemento} other {# elementos}}",
"repaired_items": "Reparado {count, plural, one {# elemento} other {# elementos}}", "repaired_items": "Reparado {count, plural, one {# elemento} other {# elementos}}",
@@ -214,7 +211,6 @@
"reset_settings_to_recent_saved": "Restablecer la configuración a la configuración guardada recientemente", "reset_settings_to_recent_saved": "Restablecer la configuración a la configuración guardada recientemente",
"scanning_library_for_changed_files": "Escanear archivos modificados en biblioteca", "scanning_library_for_changed_files": "Escanear archivos modificados en biblioteca",
"scanning_library_for_new_files": "Escanear nuevos archivos en biblioteca", "scanning_library_for_new_files": "Escanear nuevos archivos en biblioteca",
"search_jobs": "Buscar trabajo...",
"send_welcome_email": "Enviar correo de bienvenida", "send_welcome_email": "Enviar correo de bienvenida",
"server_external_domain_settings": "Dominio externo", "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 para enlaces públicos compartidos, incluidos http(s)://",
@@ -242,7 +238,6 @@
"storage_template_settings_description": "Administre la estructura de carpetas y el nombre de archivo del recurso cargado", "storage_template_settings_description": "Administre la estructura de carpetas y el nombre de archivo del recurso cargado",
"storage_template_user_label": "<code>{label}</code> es la etiqueta de almacenamiento del usuario", "storage_template_user_label": "<code>{label}</code> es la etiqueta de almacenamiento del usuario",
"system_settings": "Ajustes del Sistema", "system_settings": "Ajustes del Sistema",
"tag_cleanup_job": "Limpieza de etiquetas",
"theme_custom_css_settings": "CSS Personalizado", "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_description": "Las Hojas de Estilo (CSS) permiten personalizar el diseño de Immich.",
"theme_settings": "Ajustes Tema", "theme_settings": "Ajustes Tema",
@@ -317,7 +312,6 @@
"trash_settings_description": "Administrar la configuración de la papelera", "trash_settings_description": "Administrar la configuración de la papelera",
"untracked_files": "Archivos sin seguimiento", "untracked_files": "Archivos sin seguimiento",
"untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, cargas interrumpidas o sin procesar debido a un error", "untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, cargas interrumpidas o sin procesar debido a un error",
"user_cleanup_job": "Limpieza de usuarios",
"user_delete_delay": "La cuenta <b>{user}</b> 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 <b>{user}</b> y los archivos 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": "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_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.",
@@ -684,8 +678,8 @@
"unable_to_remove_api_key": "No se puede eliminar la clave API", "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 archivos desde el enlace compartido",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "No se pueden eliminar archivos sin conexión",
"unable_to_remove_library": "No se puede eliminar la biblioteca", "unable_to_remove_library": "No se puede eliminar la biblioteca",
"unable_to_remove_offline_files": "No se pueden eliminar archivos sin conexión",
"unable_to_remove_partner": "No se puede eliminar el invitado", "unable_to_remove_partner": "No se puede eliminar el invitado",
"unable_to_remove_reaction": "No se puede eliminar la reacción", "unable_to_remove_reaction": "No se puede eliminar la reacción",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -1088,10 +1082,10 @@
"remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del enlace compartido?", "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_title": "¿Eliminar activos?",
"remove_custom_date_range": "Eliminar intervalo de fechas personalizado", "remove_custom_date_range": "Eliminar intervalo de fechas personalizado",
"remove_deleted_assets": "Eliminar archivos sin conexión",
"remove_from_album": "Eliminar del álbum", "remove_from_album": "Eliminar del álbum",
"remove_from_favorites": "Quitar de favoritos", "remove_from_favorites": "Quitar de favoritos",
"remove_from_shared_link": "Eliminar desde enlace compartido", "remove_from_shared_link": "Eliminar desde enlace compartido",
"remove_offline_files": "Eliminar archivos sin conexión",
"remove_user": "Eliminar usuario", "remove_user": "Eliminar usuario",
"removed_api_key": "Clave API eliminada: {name}", "removed_api_key": "Clave API eliminada: {name}",
"removed_from_archive": "Eliminado del archivo", "removed_from_archive": "Eliminado del archivo",
@@ -1147,7 +1141,6 @@
"search_options": "Opciones de búsqueda", "search_options": "Opciones de búsqueda",
"search_people": "Buscar personas", "search_people": "Buscar personas",
"search_places": "Buscar lugar", "search_places": "Buscar lugar",
"search_settings": "Ajustes de la búsqueda",
"search_state": "Buscar región/estado...", "search_state": "Buscar región/estado...",
"search_tags": "Buscando etiquetas...", "search_tags": "Buscando etiquetas...",
"search_timezone": "Buscar zona horaria...", "search_timezone": "Buscar zona horaria...",
+13 -124
View File
@@ -41,22 +41,20 @@
"confirm_email_below": "Kinnitamiseks sisesta allpool \"{email}\"", "confirm_email_below": "Kinnitamiseks sisesta allpool \"{email}\"",
"confirm_reprocess_all_faces": "Kas oled kindel, et soovid kõik näod uuesti töödelda? See eemaldab kõik nimega isikud.", "confirm_reprocess_all_faces": "Kas oled kindel, et soovid kõik näod uuesti töödelda? See eemaldab kõik nimega isikud.",
"confirm_user_password_reset": "Kas oled kindel, et soovid kasutaja {user} parooli lähtestada?", "confirm_user_password_reset": "Kas oled kindel, et soovid kasutaja {user} parooli lähtestada?",
"create_job": "Lisa tööde",
"disable_login": "Keela sisselogimine", "disable_login": "Keela sisselogimine",
"duplicate_detection_job_description": "Rakenda üksustele masinõpet, et leida sarnaseid pilte. Kasutab nutiotsingut", "duplicate_detection_job_description": "Rakenda üksustele masinõpet, et tuvastada sarnaseid pilte. Kasutab nutiotsingut",
"exclusion_pattern_description": "Välistamismustrid võimaldavad ignoreerida faile ja kaustu kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.", "exclusion_pattern_description": "Välistamismustrid võimaldavad ignoreerida faile ja kaustu kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.",
"external_library_created_at": "Väline kogu (lisatud {date})", "external_library_created_at": "Väline kogu (lisatud {date})",
"external_library_management": "Väliste kogude haldus", "external_library_management": "Väliste kogude haldus",
"face_detection": "Näoavastus", "face_detection": "Näotuvastus",
"face_detection_description": "Avasta üksustest nägusid masinõppe abil. Videote puhul kasutatakse ainult pisipilti. \"Kõik\" töötleb kõik üksused uuesti. \"Puuduvad\" võtab ette üksused, mida pole veel töödeldud. Avastatud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks või uuteks isikuteks.", "face_detection_description": "Otsi üksustest nägusid masinõppe abil. Videote puhul kasutatakse ainult pisipilti. \"Kõik\" töötleb kõik üksused uuesti. \"Puuduvad\" võtab ette üksused, mida pole veel töödeldud. Leitud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks või uuteks isikuteks.",
"facial_recognition_job_description": "Grupeeri avastatud näod inimesteks. See samm käivitub siis, kui näoavastus on lõppenud. \"Kõik\" grupeerib kõik näod uuesti. \"Puuduvad\" võtab ette näod, mida pole isikuga seostatud.", "facial_recognition_job_description": "Grupeeri leitud näod inimesteks. See samm käivitub siis, kui näotuvastus on lõppenud. \"Kõik\" grupeerib kõik näod uuesti. \"Puuduvad\" võtab ette näod, mida pole isikuga seostatud.",
"failed_job_command": "Käsk {command} ebaõnnestus töötes: {job}", "failed_job_command": "Käsk {command} ebaõnnestus töötes: {job}",
"force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kõik üksused. Seda ei saa tagasi võtta ja faile ei saa taastada.", "force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kõik üksused. Seda ei saa tagasi võtta ja faile ei saa taastada.",
"forcing_refresh_library_files": "Kogu kõigi failide sundvärskendamine", "forcing_refresh_library_files": "Kogu kõigi failide sundvärskendamine",
"image_format_description": "WebP failid on väiksemad kui JPEG, aga kodeerimine on aeglasem.", "image_format_description": "WebP failid on väiksemad kui JPEG, aga kodeerimine on aeglasem.",
"image_prefer_embedded_preview": "Eelista manustatud eelvaadet", "image_prefer_embedded_preview": "Eelista manustatud eelvaadet",
"image_prefer_embedded_preview_setting_description": "Kasuta pilditöötluse sisendina võimalusel RAW fotodesse manustatud eelvaateid. See võib mõnede piltide puhul anda tulemuseks täpsemad värvid, aga eelvaate kvaliteet sõltub konkreetsest kaamerast ning pildis võib olla rohkem tihendusmüra.", "image_prefer_embedded_preview_setting_description": "Kasuta pilditöötluse sisendina võimalusel RAW fotodesse manustatud eelvaateid. See võib mõnede piltide puhul anda tulemuseks täpsemad värvid, aga eelvaate kvaliteet sõltub konkreetsest kaamerast ning pildis võib olla rohkem tihendusmüra.",
"image_prefer_wide_gamut": "Eelista laia värvigammat",
"image_prefer_wide_gamut_setting_description": "Kasuta pisipiltide jaoks Display P3. See säilitab paremini laia värviruumiga piltide erksuse, aga vanematel seadmetel ja vanemate brauseritega võivad pildid teistsugused välja näha. sRGB pildid säilitatakse värvinihete vältimiseks.", "image_prefer_wide_gamut_setting_description": "Kasuta pisipiltide jaoks Display P3. See säilitab paremini laia värviruumiga piltide erksuse, aga vanematel seadmetel ja vanemate brauseritega võivad pildid teistsugused välja näha. sRGB pildid säilitatakse värvinihete vältimiseks.",
"image_preview_format": "Eelvaate formaat", "image_preview_format": "Eelvaate formaat",
"image_preview_resolution": "Eelvaate resolutsioon", "image_preview_resolution": "Eelvaate resolutsioon",
@@ -69,13 +67,9 @@
"image_thumbnail_resolution": "Pisipildi resolutsioon", "image_thumbnail_resolution": "Pisipildi resolutsioon",
"image_thumbnail_resolution_description": "Kasutusel fotode mitmekaupa vaatamisel (ajajoon, albumi vaade, jne). Kõrgem resolutsioon säilitab rohkem detaile, aga kodeerimine võtab rohkem aega, tekitab suurema faili ning võib mõjutada rakenduse töökiirust.", "image_thumbnail_resolution_description": "Kasutusel fotode mitmekaupa vaatamisel (ajajoon, albumi vaade, jne). Kõrgem resolutsioon säilitab rohkem detaile, aga kodeerimine võtab rohkem aega, tekitab suurema faili ning võib mõjutada rakenduse töökiirust.",
"job_concurrency": "{job} samaaegsus", "job_concurrency": "{job} samaaegsus",
"job_created": "Tööde lisatud",
"job_not_concurrency_safe": "Seda töödet pole ohutu samaaegselt käivitada.",
"job_settings": "Tööte seaded", "job_settings": "Tööte seaded",
"job_settings_description": "Halda töödete samaaegsust", "job_settings_description": "Halda töödete samaaegsust",
"job_status": "Tööte seisund", "job_status": "Tööte seisund",
"jobs_delayed": "{jobCount, plural, other {# edasi lükatud}}",
"jobs_failed": "{jobCount, plural, other {# ebaõnnestus}}",
"library_created": "Lisatud kogu: {library}", "library_created": "Lisatud kogu: {library}",
"library_cron_expression": "Cron avaldis", "library_cron_expression": "Cron avaldis",
"library_cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. <link>Crontab Guru</link>", "library_cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. <link>Crontab Guru</link>",
@@ -87,7 +81,6 @@
"library_scanning_enable_description": "Luba kogu perioodiline skaneerimine", "library_scanning_enable_description": "Luba kogu perioodiline skaneerimine",
"library_settings": "Väline kogu", "library_settings": "Väline kogu",
"library_settings_description": "Halda välise kogu seadeid", "library_settings_description": "Halda välise kogu seadeid",
"library_tasks_description": "Soorita kogu toiminguid",
"library_watching_enable_description": "Jälgi välises kogus failide muudatusi", "library_watching_enable_description": "Jälgi välises kogus failide muudatusi",
"library_watching_settings": "Kogu jälgimine (EKSPERIMENTAALNE)", "library_watching_settings": "Kogu jälgimine (EKSPERIMENTAALNE)",
"library_watching_settings_description": "Jälgi automaatselt muutunud faile", "library_watching_settings_description": "Jälgi automaatselt muutunud faile",
@@ -96,26 +89,23 @@
"logging_settings": "Logimine", "logging_settings": "Logimine",
"machine_learning_clip_model": "CLIP mudel", "machine_learning_clip_model": "CLIP mudel",
"machine_learning_clip_model_description": "CLIP mudeli nimi, mis on loetletud <link>siin</link>. Pane tähele, et mudeli muutmisel pead kõigi piltide peal nutiotsingu tööte uuesti käivitama.", "machine_learning_clip_model_description": "CLIP mudeli nimi, mis on loetletud <link>siin</link>. Pane tähele, et mudeli muutmisel pead kõigi piltide peal nutiotsingu tööte uuesti käivitama.",
"machine_learning_duplicate_detection": "Duplikaatide leidmine", "machine_learning_duplicate_detection": "Duplikaatide tuvastus",
"machine_learning_duplicate_detection_enabled": "Luba duplikaatide leidmine", "machine_learning_duplicate_detection_enabled": "Luba duplikaatide tuvastus",
"machine_learning_duplicate_detection_enabled_description": "Kui keelatud, dedubleeritakse siiski täpselt identsed üksused.", "machine_learning_duplicate_detection_enabled_description": "Kui keelatud, dedubleeritakse siiski täpselt identsed üksused.",
"machine_learning_duplicate_detection_setting_description": "Kasuta CLIP-manuseid, et leida tõenäoliseid duplikaate", "machine_learning_duplicate_detection_setting_description": "Kasuta CLIP-manuseid, et leida tõenäoliseid duplikaate",
"machine_learning_enabled": "Luba masinõpe", "machine_learning_enabled": "Luba masinõpe",
"machine_learning_enabled_description": "Kui keelatud, lülitatakse kõik masinõppe funktsioonid välja, sõltumata allolevatest seadetest.", "machine_learning_enabled_description": "Kui keelatud, lülitatakse kõik masinõppe funktsioonid välja, sõltumata allolevatest seadetest.",
"machine_learning_facial_recognition": "Näotuvastus", "machine_learning_facial_recognition": "Näotuvastus",
"machine_learning_facial_recognition_description": "Avasta, tuvasta ja grupeeri piltidel näod", "machine_learning_facial_recognition_description": "Otsi, tuvasta ja grupeeri piltidel näod",
"machine_learning_facial_recognition_model": "Näotuvastuse mudel", "machine_learning_facial_recognition_model": "Näotuvastuse mudel",
"machine_learning_facial_recognition_model_description": "Mudelid on järjestatud suuruse järgi kahanevalt. Suuremad mudelid on aeglasemad ja kasutavad rohkem mälu, kuid annavad parema tulemuse. Mudeli muutmisel tuleb näoavastuse tööde kõigi piltide peal uuesti käivitada.", "machine_learning_facial_recognition_model_description": "Mudelid on järjestatud suuruse järgi kahanevalt. Suuremad mudelid on aeglasemad ja kasutavad rohkem mälu, kuid annavad parema tulemuse. Mudeli muutmisel tuleb näotuvastuse tööde kõigi piltide peal uuesti käivitada.",
"machine_learning_facial_recognition_setting": "Luba näotuvastus", "machine_learning_facial_recognition_setting": "Luba näotuvastus",
"machine_learning_facial_recognition_setting_description": "Kui keelatud, siis ei kodeerita pilte näotuvastuse jaoks ning isikute sektsioon Avasta lehel jääb tühjaks.", "machine_learning_max_detection_distance": "Maksimaalne tuvastuskaugus",
"machine_learning_max_detection_distance": "Maksimaalne avastuskaugus", "machine_learning_max_detection_distance_description": "Maksimaalne kaugus kahe pildi vahel, mille puhul loetakse nad duplikaatideks, vahemikus 0.001-0.1. Kõrgemad väärtused tuvastavad rohkem duplikaate, aga võivad anda valepositiivseid.",
"machine_learning_max_detection_distance_description": "Maksimaalne kaugus kahe pildi vahel, mille puhul loetakse nad duplikaatideks, vahemikus 0.001-0.1. Kõrgemad väärtused leiavad rohkem duplikaate, aga võib esineda valepositiivseid.",
"machine_learning_max_recognition_distance": "Maksimaalne tuvastuskaugus",
"machine_learning_max_recognition_distance_description": "Maksimaalne kaugus kahe näo vahel, mida tuleks lugeda samaks isikuks, vahemikus 0-2. Selle vähendamine aitab vältida erinevate inimeste samaks isikuks märkimist ja tõstmine aitab vältida sama inimese kaheks erinevaks isikuks märkimist. Pane tähele, et kaht isikut ühendada on lihtsam kui üht isikut kaheks eraldada, seega võimalusel kasuta madalamat lävendit.", "machine_learning_max_recognition_distance_description": "Maksimaalne kaugus kahe näo vahel, mida tuleks lugeda samaks isikuks, vahemikus 0-2. Selle vähendamine aitab vältida erinevate inimeste samaks isikuks märkimist ja tõstmine aitab vältida sama inimese kaheks erinevaks isikuks märkimist. Pane tähele, et kaht isikut ühendada on lihtsam kui üht isikut kaheks eraldada, seega võimalusel kasuta madalamat lävendit.",
"machine_learning_min_detection_score": "Minimaalne avastusskoor", "machine_learning_min_detection_score_description": "Minimaalne usaldusskoor näo tuvastamiseks, vahemikus 0-1. Madalamad väärtused leiavad rohkem nägusid, kuid võib esineda valepositiivseid.",
"machine_learning_min_detection_score_description": "Minimaalne usaldusskoor näo avastamiseks, vahemikus 0-1. Madalamad väärtused leiavad rohkem nägusid, kuid võib esineda valepositiivseid.", "machine_learning_min_recognized_faces": "Minimaalne leitud nägude arv",
"machine_learning_min_recognized_faces": "Minimaalne tuvastatud nägude arv", "machine_learning_min_recognized_faces_description": "Minimaalne leitud nägude arv, mida saab isikuks grupeerida. Selle suurendamine teeb näotuvastuse täpsemaks, kuid suureneb tõenäosus, et nägu ei seostata ühegi isikuga.",
"machine_learning_min_recognized_faces_description": "Minimaalne tuvastatud nägude arv, mida saab isikuks grupeerida. Selle suurendamine teeb näotuvastuse täpsemaks, kuid suureneb tõenäosus, et nägu ei seostata ühegi isikuga.",
"machine_learning_settings": "Masinõppe seaded", "machine_learning_settings": "Masinõppe seaded",
"machine_learning_settings_description": "Halda masinõppe funktsioone ja seadeid", "machine_learning_settings_description": "Halda masinõppe funktsioone ja seadeid",
"machine_learning_smart_search": "Nutiotsing", "machine_learning_smart_search": "Nutiotsing",
@@ -123,30 +113,17 @@
"machine_learning_smart_search_enabled": "Luba nutiotsing", "machine_learning_smart_search_enabled": "Luba nutiotsing",
"machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.", "machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.",
"machine_learning_url_description": "Masinõppe serveri URL", "machine_learning_url_description": "Masinõppe serveri URL",
"manage_concurrency": "Halda samaaegsust",
"manage_log_settings": "Halda logi seadeid", "manage_log_settings": "Halda logi seadeid",
"map_dark_style": "Tume stiil", "map_dark_style": "Tume stiil",
"map_enable_description": "Luba kaardi funktsioonid",
"map_gps_settings": "Kaardi ja GPS-i seaded", "map_gps_settings": "Kaardi ja GPS-i seaded",
"map_gps_settings_description": "Halda kaardi ja GPS-i (pöördgeokodeerimise) seadeid",
"map_implications": "Kaardifunktsioon kasutab välist kaarditeenust (tiles.immich.cloud)",
"map_light_style": "Hele stiil", "map_light_style": "Hele stiil",
"map_manage_reverse_geocoding_settings": "Halda <link>pöördgeokodeerimise</link> seadeid",
"map_reverse_geocoding": "Pöördgeokodeerimine",
"map_reverse_geocoding_enable_description": "Luba pöördgeokodeerimine",
"map_reverse_geocoding_settings": "Pöördgeokodeerimise seaded",
"map_settings": "Kaart", "map_settings": "Kaart",
"map_settings_description": "Halda kaardi seadeid", "map_settings_description": "Halda kaardi seadeid",
"map_style_description": "Kaarditeema style.json URL",
"metadata_extraction_job": "Metaandmete eraldamine", "metadata_extraction_job": "Metaandmete eraldamine",
"metadata_extraction_job_description": "Eralda igast üksusest metaandmed, nagu GPS-koordinaadid, näod ja resolutsioon", "metadata_extraction_job_description": "Eralda igast üksusest metaandmed, nagu GPS-koordinaadid, näod ja resolutsioon",
"metadata_faces_import_setting": "Luba nägude import",
"metadata_settings": "Metaandmete seaded",
"metadata_settings_description": "Halda metaandmete seadeid",
"migration_job": "Migratsioon", "migration_job": "Migratsioon",
"migration_job_description": "Migreeri üksuste ja nägude pisipildid uusimale kaustastruktuurile", "migration_job_description": "Migreeri üksuste ja nägude pisipildid uusimale kaustastruktuurile",
"note_cannot_be_changed_later": "MÄRKUS: Seda ei saa hiljem muuta!", "note_cannot_be_changed_later": "MÄRKUS: Seda ei saa hiljem muuta!",
"note_unlimited_quota": "Märkus: Piiramatu kvoodi jaoks sisesta 0",
"notification_email_from_address": "Saatja aadress", "notification_email_from_address": "Saatja aadress",
"notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server <noreply@example.com>\"", "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server <noreply@example.com>\"",
"notification_email_host_description": "E-posti serveri host (nt. smtp.immich.app)", "notification_email_host_description": "E-posti serveri host (nt. smtp.immich.app)",
@@ -163,27 +140,17 @@
"notification_enable_email_notifications": "Luba e-posti teel teavitused", "notification_enable_email_notifications": "Luba e-posti teel teavitused",
"notification_settings": "Teavituse seaded", "notification_settings": "Teavituse seaded",
"notification_settings_description": "Halda teavituste seadeid, sh. e-posti teel", "notification_settings_description": "Halda teavituste seadeid, sh. e-posti teel",
"oauth_auto_launch": "Automaatne käivitamine",
"oauth_auto_launch_description": "Alusta OAuth autentimist automaatselt sisselogimise lehele jõudmisel",
"oauth_auto_register": "Automaatne registreerimine",
"oauth_auto_register_description": "Registreeri uued kasutajad automaatselt OAuth abil sisselogimisel",
"oauth_button_text": "Nupu tekst", "oauth_button_text": "Nupu tekst",
"oauth_client_id": "Kliendi ID", "oauth_client_id": "Kliendi ID",
"oauth_client_secret": "Kliendi saladus", "oauth_client_secret": "Kliendi saladus",
"oauth_enable_description": "Sisene OAuth abil", "oauth_enable_description": "Sisene OAuth abil",
"oauth_issuer_url": "Väljastaja URL", "oauth_issuer_url": "Väljastaja URL",
"oauth_mobile_redirect_uri": "Mobiilne ümbersuunamise URI",
"oauth_profile_signing_algorithm": "Profiili allkirjastamise algoritm",
"oauth_profile_signing_algorithm_description": "Algoritm, mida kasutatakse kasutajaprofiili allkirjastamiseks.",
"oauth_scope": "Skoop",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Halda OAuth sisselogimise seadeid", "oauth_settings_description": "Halda OAuth sisselogimise seadeid",
"oauth_signing_algorithm": "Allkirjastamise algoritm",
"password_enable_description": "Logi sisse e-posti aadressi ja parooliga", "password_enable_description": "Logi sisse e-posti aadressi ja parooliga",
"password_settings": "Parooliga sisselogimine", "password_settings": "Parooliga sisselogimine",
"password_settings_description": "Halda parooliga sisselogimise seadeid", "password_settings_description": "Halda parooliga sisselogimise seadeid",
"paths_validated_successfully": "Kõik teed edukalt valideeritud", "paths_validated_successfully": "Kõik teed edukalt valideeritud",
"person_cleanup_job": "Isikute korrastamine",
"quota_size_gib": "Kvoot (GiB)", "quota_size_gib": "Kvoot (GiB)",
"refreshing_all_libraries": "Kõikide kogude värskendamine", "refreshing_all_libraries": "Kõikide kogude värskendamine",
"registration_description": "Kuna sa oled süsteemis esimene kasutaja, määratakse sind administraatoriks, ning sa saad lisada täiendavaid kasutajaid.", "registration_description": "Kuna sa oled süsteemis esimene kasutaja, määratakse sind administraatoriks, ning sa saad lisada täiendavaid kasutajaid.",
@@ -204,10 +171,6 @@
"storage_template_migration_info": "Malli muudatused rakenduvad ainult uutele üksustele. Et rakendada malli tagasiulatuvalt olemasolevatele üksustele, käivita <link>{job}</link>.", "storage_template_migration_info": "Malli muudatused rakenduvad ainult uutele üksustele. Et rakendada malli tagasiulatuvalt olemasolevatele üksustele, käivita <link>{job}</link>.",
"storage_template_settings_description": "Halda üleslaaditud üksuse kaustastruktuuri ja failinime", "storage_template_settings_description": "Halda üleslaaditud üksuse kaustastruktuuri ja failinime",
"system_settings": "Süsteemi seaded", "system_settings": "Süsteemi seaded",
"tag_cleanup_job": "Siltide korrastamine",
"theme_custom_css_settings": "Kohandatud CSS",
"theme_custom_css_settings_description": "Cascading Style Sheets lubab Immich'i kujunduse kohandamist.",
"theme_settings": "Teema seaded",
"theme_settings_description": "Halda Immich'i veebiliidese kohandamist", "theme_settings_description": "Halda Immich'i veebiliidese kohandamist",
"thumbnail_generation_job": "Genereeri pisipildid", "thumbnail_generation_job": "Genereeri pisipildid",
"thumbnail_generation_job_description": "Genereeri iga üksuse kohta suur, väike ja udustatud pisipilt ning iga isiku kohta pisipilt", "thumbnail_generation_job_description": "Genereeri iga üksuse kohta suur, väike ja udustatud pisipilt ning iga isiku kohta pisipilt",
@@ -266,18 +229,13 @@
"transcoding_video_codec_description": "VP9 on võimekas ja veebiga ühilduv, aga transkodeerimine võtab kauem aega. HEVC on sarnase jõudluse, aga mitte nii hea veebiga ühilduvusega. H.264 on laialt ühilduv ja transkodeerimine on kiire, aga tulemuseks on suuremad failid. AV1 on kõige võimekam koodek, aga pole vanematel seadmetel toetatud.", "transcoding_video_codec_description": "VP9 on võimekas ja veebiga ühilduv, aga transkodeerimine võtab kauem aega. HEVC on sarnase jõudluse, aga mitte nii hea veebiga ühilduvusega. H.264 on laialt ühilduv ja transkodeerimine on kiire, aga tulemuseks on suuremad failid. AV1 on kõige võimekam koodek, aga pole vanematel seadmetel toetatud.",
"trash_number_of_days": "Päevade arv", "trash_number_of_days": "Päevade arv",
"trash_number_of_days_description": "Päevade arv, kui kaua hoida üksusi prügikastis enne nende lõplikku kustutamist", "trash_number_of_days_description": "Päevade arv, kui kaua hoida üksusi prügikastis enne nende lõplikku kustutamist",
"user_cleanup_job": "Kasutajate korrastamine",
"user_delete_delay": "Kasutaja <b>{user}</b> konto ja üksuste lõplik kustutamine on planeeritud {delay, plural, one {# päeva} other {# päeva}} pärast.", "user_delete_delay": "Kasutaja <b>{user}</b> konto ja üksuste lõplik kustutamine on planeeritud {delay, plural, one {# päeva} other {# päeva}} pärast.",
"user_delete_delay_settings_description": "Päevade arv, pärast mida kustutatakse eemaldatud kasutaja konto ja üksused jäädavalt. Kasutajate kustutamise tööde käivitub keskööl, et otsida kustutamiseks valmis kasutajaid. Selle seadistuse muudatused rakenduvad järgmisel käivitumisel.", "user_delete_delay_settings_description": "Päevade arv, pärast mida kustutatakse eemaldatud kasutaja konto ja üksused jäädavalt. Kasutajate kustutamise tööde käivitub keskööl, et otsida kustutamiseks valmis kasutajaid. Selle seadistuse muudatused rakenduvad järgmisel käivitumisel.",
"user_delete_immediately": "Kasutaja <b>{user}</b> konto ja üksused suunatakse <b>koheselt</b> jäädavale kustutamisele.", "user_delete_immediately": "Kasutaja <b>{user}</b> konto ja üksused suunatakse <b>koheselt</b> jäädavale kustutamisele.",
"user_delete_immediately_checkbox": "Suuna kasutaja ja üksused jäädavale kustutamisele", "user_delete_immediately_checkbox": "Suuna kasutaja ja üksused jäädavale kustutamisele",
"user_management": "Kasutajate haldus",
"user_password_has_been_reset": "Kasutaja parool on lähtestatud:", "user_password_has_been_reset": "Kasutaja parool on lähtestatud:",
"user_password_reset_description": "Sisesta kasutajale ajutine parool ja teavita teda, et järgmisel sisselogimisel tuleb parool ära muuta.", "user_password_reset_description": "Sisesta kasutajale ajutine parool ja teavita teda, et järgmisel sisselogimisel tuleb parool ära muuta.",
"user_restore_description": "Kasutaja <b>{user}</b> konto taastatakse.", "user_restore_description": "Kasutaja <b>{user}</b> konto taastatakse.",
"user_restore_scheduled_removal": "Taasta kasutaja - eemaldamine planeeritud {date, date, long}",
"user_settings": "Kasutajate seaded",
"user_settings_description": "Halda kasutajate seadeid",
"user_successfully_removed": "Kasutaja {email} on eemaldatud.", "user_successfully_removed": "Kasutaja {email} on eemaldatud.",
"version_check_enabled_description": "Luba versioonikontroll", "version_check_enabled_description": "Luba versioonikontroll",
"version_check_implications": "Versioonikontroll vajab perioodilist ühendumist github.com-iga", "version_check_implications": "Versioonikontroll vajab perioodilist ühendumist github.com-iga",
@@ -312,17 +270,11 @@
"all_albums": "Kõik albumid", "all_albums": "Kõik albumid",
"all_people": "Kõik isikud", "all_people": "Kõik isikud",
"all_videos": "Kõik videod", "all_videos": "Kõik videod",
"anti_clockwise": "Vastupäeva",
"api_key": "API võti",
"api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.", "api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.",
"api_key_empty": "Su API võtme nimi ei tohiks olla tühi",
"api_keys": "API võtmed",
"app_settings": "Rakenduse seaded",
"archive": "Arhiiv", "archive": "Arhiiv",
"archive_or_unarchive_photo": "Arhiveeri või taasta foto", "archive_or_unarchive_photo": "Arhiveeri või taasta foto",
"archive_size": "Arhiivi suurus", "archive_size": "Arhiivi suurus",
"archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)", "archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)",
"are_these_the_same_person": "Kas need on sama isik?",
"are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?", "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?",
"asset_added_to_album": "Lisatud albumisse", "asset_added_to_album": "Lisatud albumisse",
"asset_adding_to_album": "Albumisse lisamine...", "asset_adding_to_album": "Albumisse lisamine...",
@@ -355,19 +307,13 @@
"bulk_delete_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid kustutatakse jäädavalt. Seda tegevust ei saa tagasi võtta!", "bulk_delete_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid kustutatakse jäädavalt. Seda tegevust ei saa tagasi võtta!",
"bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.", "bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.",
"bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.", "bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.",
"buy": "Osta Immich",
"camera": "Kaamera", "camera": "Kaamera",
"camera_brand": "Kaamera mark", "camera_brand": "Kaamera mark",
"camera_model": "Kaamera mudel", "camera_model": "Kaamera mudel",
"cancel": "Katkesta", "cancel": "Katkesta",
"cancel_search": "Katkesta otsing",
"cannot_merge_people": "Ei saa isikuid ühendada", "cannot_merge_people": "Ei saa isikuid ühendada",
"cannot_undo_this_action": "Sa ei saa seda tagasi võtta!", "cannot_undo_this_action": "Sa ei saa seda tagasi võtta!",
"cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus", "cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus",
"change_date": "Muuda kuupäeva",
"change_expiration_time": "Muuda aegumisaega",
"change_location": "Muuda asukohta",
"change_name": "Muuda nime",
"change_password": "Parooli muutmine", "change_password": "Parooli muutmine",
"change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.", "change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.",
"change_your_password": "Muuda oma parooli", "change_your_password": "Muuda oma parooli",
@@ -376,10 +322,6 @@
"check_logs": "Vaata logisid", "check_logs": "Vaata logisid",
"choose_matching_people_to_merge": "Vali kattuvad isikud, mida ühendada", "choose_matching_people_to_merge": "Vali kattuvad isikud, mida ühendada",
"city": "Linn", "city": "Linn",
"clear": "Tühjenda",
"clear_all": "Tühjenda kõik",
"clear_all_recent_searches": "Tühjenda hiljutised otsingud",
"clear_value": "Tühjenda väärtus",
"clockwise": "Päripäeva", "clockwise": "Päripäeva",
"close": "Sulge", "close": "Sulge",
"color": "Värv", "color": "Värv",
@@ -393,7 +335,6 @@
"confirm_delete_shared_link": "Kas oled kindel, et soovid selle jagatud lingi kustutada?", "confirm_delete_shared_link": "Kas oled kindel, et soovid selle jagatud lingi kustutada?",
"confirm_password": "Kinnita parool", "confirm_password": "Kinnita parool",
"context": "Kontekst", "context": "Kontekst",
"continue": "Jätka",
"copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.", "copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.",
"copied_to_clipboard": "Kopeeritud lõikelauale!", "copied_to_clipboard": "Kopeeritud lõikelauale!",
"copy_error": "Kopeeri viga", "copy_error": "Kopeeri viga",
@@ -442,9 +383,7 @@
"delete_user": "Kustuta kasutaja", "delete_user": "Kustuta kasutaja",
"deleted_shared_link": "Jagatud link kustutatud", "deleted_shared_link": "Jagatud link kustutatud",
"description": "Kirjeldus", "description": "Kirjeldus",
"details": "Üksikasjad",
"direction": "Suund", "direction": "Suund",
"disallow_edits": "Keela muutmine",
"discover": "Avasta", "discover": "Avasta",
"display_options": "Kuva valikud", "display_options": "Kuva valikud",
"display_original_photos_setting_description": "Eelista üksuse vaatamisel pisipildile algset fotot, kui see on veebiga ühilduv. See võib mõjutada fotode kuvamise kiirust.", "display_original_photos_setting_description": "Eelista üksuse vaatamisel pisipildile algset fotot, kui see on veebiga ühilduv. See võib mõjutada fotode kuvamise kiirust.",
@@ -515,7 +454,6 @@
"import_path_already_exists": "See imporditee on juba olemas.", "import_path_already_exists": "See imporditee on juba olemas.",
"incorrect_email_or_password": "Vale e-posti aadress või parool", "incorrect_email_or_password": "Vale e-posti aadress või parool",
"profile_picture_transparent_pixels": "Profiilipildis ei tohi olla läbipaistvaid piksleid. Palun suumi sisse ja/või liiguta pilti.", "profile_picture_transparent_pixels": "Profiilipildis ei tohi olla läbipaistvaid piksleid. Palun suumi sisse ja/või liiguta pilti.",
"quota_higher_than_disk_size": "Määratud kvoot on suurem kui kettamaht",
"unable_to_add_album_users": "Kasutajate lisamine albumisse ebaõnnestus", "unable_to_add_album_users": "Kasutajate lisamine albumisse ebaõnnestus",
"unable_to_add_assets_to_shared_link": "Üksuste jagatud lingile lisamine ebaõnnestus", "unable_to_add_assets_to_shared_link": "Üksuste jagatud lingile lisamine ebaõnnestus",
"unable_to_add_comment": "Kommentaari lisamine ebaõnnestus", "unable_to_add_comment": "Kommentaari lisamine ebaõnnestus",
@@ -525,7 +463,6 @@
"unable_to_add_remove_archive": "{archived, select, true {Üksuse arhiivist taastamine} other {Üksuse arhiveerimine}} ebaõnnestus", "unable_to_add_remove_archive": "{archived, select, true {Üksuse arhiivist taastamine} other {Üksuse arhiveerimine}} ebaõnnestus",
"unable_to_add_remove_favorites": "Üksuse {favorite, select, true {lemmikuks lisamine} other {lemmikutest eemaldamine}} ebaõnnestus", "unable_to_add_remove_favorites": "Üksuse {favorite, select, true {lemmikuks lisamine} other {lemmikutest eemaldamine}} ebaõnnestus",
"unable_to_archive_unarchive": "{archived, select, true {Arhiveerimine} other {Arhiivist taastamine}} ebaõnnestus", "unable_to_archive_unarchive": "{archived, select, true {Arhiveerimine} other {Arhiivist taastamine}} ebaõnnestus",
"unable_to_change_album_user_role": "Kasutaja rolli albumis muutmine ebaõnnestus",
"unable_to_change_date": "Kuupäeva muutmine ebaõnnestus", "unable_to_change_date": "Kuupäeva muutmine ebaõnnestus",
"unable_to_change_favorite": "Üksuse lemmiku staatuse muutmine ebaõnnestus", "unable_to_change_favorite": "Üksuse lemmiku staatuse muutmine ebaõnnestus",
"unable_to_change_location": "Asukoha muutmine ebaõnnestus", "unable_to_change_location": "Asukoha muutmine ebaõnnestus",
@@ -599,32 +536,23 @@
"expired": "Aegunud", "expired": "Aegunud",
"expires_date": "Aegub {date}", "expires_date": "Aegub {date}",
"explore": "Avasta", "explore": "Avasta",
"export": "Ekspordi",
"export_as_json": "Ekspordi JSON-formaati", "export_as_json": "Ekspordi JSON-formaati",
"extension": "Laiend", "extension": "Laiend",
"external": "Väline",
"external_libraries": "Välised kogud",
"face_unassigned": "Seostamata", "face_unassigned": "Seostamata",
"favorite": "Lemmik", "favorite": "Lemmik",
"favorites": "Lemmikud", "favorites": "Lemmikud",
"feature_photo_updated": "Esiletõstetud foto muudetud", "feature_photo_updated": "Esiletõstetud foto muudetud",
"features": "Funktsioonid",
"features_setting_description": "Halda rakenduse funktsioone",
"file_name": "Failinimi", "file_name": "Failinimi",
"file_name_or_extension": "Failinimi või -laiend", "file_name_or_extension": "Failinimi või -laiend",
"filename": "Failinimi", "filename": "Failinimi",
"filetype": "Failitüüp", "filetype": "Failitüüp",
"filter_people": "Filtreeri isikuid", "filter_people": "Filtreeri isikuid",
"find_them_fast": "Leia teda kiiresti nime järgi otsides",
"folders": "Kaustad", "folders": "Kaustad",
"folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine", "folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine",
"force_re-scan_library_files": "Sundskaneeri kogu kõik failid uuesti", "force_re-scan_library_files": "Sundskaneeri kogu kõik failid uuesti",
"forward": "Edasi", "forward": "Edasi",
"general": "Üldine", "general": "Üldine",
"get_help": "Küsi abi",
"getting_started": "Alustamine",
"go_back": "Tagasi", "go_back": "Tagasi",
"go_to_search": "Otsingusse",
"group_albums_by": "Grupeeri albumid...", "group_albums_by": "Grupeeri albumid...",
"group_no": "Ära grupeeri", "group_no": "Ära grupeeri",
"group_owner": "Grupeeri omaniku kaupa", "group_owner": "Grupeeri omaniku kaupa",
@@ -647,7 +575,6 @@
"immich_logo": "Immich'i logo", "immich_logo": "Immich'i logo",
"immich_web_interface": "Immich'i veebiliides", "immich_web_interface": "Immich'i veebiliides",
"import_from_json": "Impordi JSON-formaadist", "import_from_json": "Impordi JSON-formaadist",
"in_albums": "{count, plural, one {# albumis} other {# albumis}}",
"in_archive": "Arhiivis", "in_archive": "Arhiivis",
"info": "Info", "info": "Info",
"interval": { "interval": {
@@ -668,13 +595,9 @@
"latest_version": "Uusim versioon", "latest_version": "Uusim versioon",
"latitude": "Laiuskraad", "latitude": "Laiuskraad",
"leave": "Lahku", "leave": "Lahku",
"let_others_respond": "Luba teistel vastata",
"library": "Kogu", "library": "Kogu",
"library_options": "Kogu seaded", "library_options": "Kogu seaded",
"light": "Hele",
"link_options": "Lingi valikud",
"list": "Loend", "list": "Loend",
"loading": "Laadimine",
"loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus", "loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus",
"log_out": "Logi välja", "log_out": "Logi välja",
"log_out_all_devices": "Logi kõigist seadmetest välja", "log_out_all_devices": "Logi kõigist seadmetest välja",
@@ -684,19 +607,15 @@
"logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?", "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?",
"logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?", "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?",
"longitude": "Pikkuskraad", "longitude": "Pikkuskraad",
"look": "Välimus",
"make": "Mark", "make": "Mark",
"manage_shared_links": "Halda jagatud linke", "manage_shared_links": "Halda jagatud linke",
"manage_sharing_with_partners": "Halda partneritega jagamist", "manage_sharing_with_partners": "Halda partneritega jagamist",
"manage_the_app_settings": "Halda rakenduse seadeid",
"manage_your_account": "Halda oma kontot", "manage_your_account": "Halda oma kontot",
"manage_your_api_keys": "Halda oma API võtmeid", "manage_your_api_keys": "Halda oma API võtmeid",
"manage_your_devices": "Halda oma autenditud seadmeid", "manage_your_devices": "Halda oma autenditud seadmeid",
"map": "Kaart", "map": "Kaart",
"map_settings": "Kaardi seaded", "map_settings": "Kaardi seaded",
"media_type": "Meedia tüüp",
"memories": "Mälestused", "memories": "Mälestused",
"memories_setting_description": "Halda, mida sa oma mälestustes näed",
"memory": "Mälestus", "memory": "Mälestus",
"menu": "Menüü", "menu": "Menüü",
"merge": "Ühenda", "merge": "Ühenda",
@@ -723,24 +642,17 @@
"next_memory": "Järgmine mälestus", "next_memory": "Järgmine mälestus",
"no": "Ei", "no": "Ei",
"no_albums_message": "Lisa album fotode ja videote organiseerimiseks", "no_albums_message": "Lisa album fotode ja videote organiseerimiseks",
"no_albums_with_name_yet": "Paistab, et sul pole veel ühtegi selle nimega albumit.",
"no_albums_yet": "Paistab, et sul pole veel ühtegi albumit.",
"no_archived_assets_message": "Arhiveeri fotod ja videod, et neid Fotod vaatest peita", "no_archived_assets_message": "Arhiveeri fotod ja videod, et neid Fotod vaatest peita",
"no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS", "no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS",
"no_duplicates_found": "Ühtegi duplikaati ei leitud.", "no_duplicates_found": "Ühtegi duplikaati ei leitud.",
"no_exif_info_available": "Exif info pole saadaval", "no_exif_info_available": "Exif info pole saadaval",
"no_explore_results_message": "Oma kogu avastamiseks laadi üles rohkem fotosid.",
"no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida", "no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida",
"no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks", "no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks",
"no_results": "Vasteid pole",
"no_results_description": "Proovi sünonüümi või üldisemat märksõna",
"no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada", "no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada",
"notes": "Märkused",
"notification_toggle_setting_description": "Luba e-posti teel teavitused", "notification_toggle_setting_description": "Luba e-posti teel teavitused",
"notifications": "Teavitused", "notifications": "Teavitused",
"notifications_setting_description": "Halda teavitusi", "notifications_setting_description": "Halda teavitusi",
"oauth": "OAuth", "oauth": "OAuth",
"ok": "Ok",
"oldest_first": "Vanemad eespool", "oldest_first": "Vanemad eespool",
"onboarding_theme_description": "Vali oma serverile värviteema. Saad seda hiljem seadetes muuta.", "onboarding_theme_description": "Vali oma serverile värviteema. Saad seda hiljem seadetes muuta.",
"onboarding_welcome_user": "Tere tulemast, {user}", "onboarding_welcome_user": "Tere tulemast, {user}",
@@ -748,13 +660,11 @@
"only_refreshes_modified_files": "Värskendab ainult muudetud failid", "only_refreshes_modified_files": "Värskendab ainult muudetud failid",
"open_in_map_view": "Ava kaardi vaates", "open_in_map_view": "Ava kaardi vaates",
"open_in_openstreetmap": "Ava OpenStreetMap", "open_in_openstreetmap": "Ava OpenStreetMap",
"open_the_search_filters": "Ava otsingufiltrid",
"options": "Valikud", "options": "Valikud",
"or": "või", "or": "või",
"organize_your_library": "Korrasta oma kogu", "organize_your_library": "Korrasta oma kogu",
"original": "originaal", "original": "originaal",
"other_devices": "Muud seadmed", "other_devices": "Muud seadmed",
"other_variables": "Muud muutujad",
"owned": "Minu omad", "owned": "Minu omad",
"owner": "Omanik", "owner": "Omanik",
"partner": "Partner", "partner": "Partner",
@@ -789,8 +699,6 @@
"permanently_deleted_asset": "Üksus jäädavalt kustutatud", "permanently_deleted_asset": "Üksus jäädavalt kustutatud",
"permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", "permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud",
"person": "Isik", "person": "Isik",
"person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}",
"photo_shared_all_users": "Paistab, et oled oma fotosid kõigi kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.",
"photos": "Fotod", "photos": "Fotod",
"photos_and_videos": "Fotod ja videod", "photos_and_videos": "Fotod ja videod",
"photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotot}}", "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotot}}",
@@ -831,8 +739,6 @@
"purchase_panel_info_1": "Immich'i arendamine nõuab palju aega ja vaeva ning meie täiskohaga insenerid töötavad selle nimel, et teha see nii heaks kui vähegi võimalik. Meie missiooniks on muuta avatud lähtekoodiga tarkvara ja eetilised äritavad arendajatele jätkusuutlikuks sissetulekuallikaks ning luua privaatsust austav ökosüsteem, mis pakub tõelisi alternatiive ekspluatatiivsetele pilveteenustele.", "purchase_panel_info_1": "Immich'i arendamine nõuab palju aega ja vaeva ning meie täiskohaga insenerid töötavad selle nimel, et teha see nii heaks kui vähegi võimalik. Meie missiooniks on muuta avatud lähtekoodiga tarkvara ja eetilised äritavad arendajatele jätkusuutlikuks sissetulekuallikaks ning luua privaatsust austav ökosüsteem, mis pakub tõelisi alternatiive ekspluatatiivsetele pilveteenustele.",
"purchase_panel_info_2": "Kuna oleme otsustanud maksumüüre mitte lisada, ei anna see ost sulle Immich'is lisavõimalusi. Me loodame Immich'i jätkuvaks arenduseks sinusuguste kasutajate toetusele.", "purchase_panel_info_2": "Kuna oleme otsustanud maksumüüre mitte lisada, ei anna see ost sulle Immich'is lisavõimalusi. Me loodame Immich'i jätkuvaks arenduseks sinusuguste kasutajate toetusele.",
"purchase_panel_title": "Toeta projekti", "purchase_panel_title": "Toeta projekti",
"purchase_per_server": "Serveri kohta",
"purchase_per_user": "Kasutaja kohta",
"purchase_remove_product_key": "Eemalda tootevõti", "purchase_remove_product_key": "Eemalda tootevõti",
"purchase_remove_product_key_prompt": "Kas oled kindel, et soovid tootevõtme eemaldada?", "purchase_remove_product_key_prompt": "Kas oled kindel, et soovid tootevõtme eemaldada?",
"purchase_remove_server_product_key": "Eemalda serveri tootevõti", "purchase_remove_server_product_key": "Eemalda serveri tootevõti",
@@ -841,12 +747,10 @@
"purchase_server_description_2": "Toetaja staatus", "purchase_server_description_2": "Toetaja staatus",
"purchase_server_title": "Server", "purchase_server_title": "Server",
"purchase_settings_server_activated": "Serveri tootevõtit haldab administraator", "purchase_settings_server_activated": "Serveri tootevõtit haldab administraator",
"reaction_options": "Reaktsiooni valikud",
"read_changelog": "Vaata muudatuste ülevaadet", "read_changelog": "Vaata muudatuste ülevaadet",
"reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}", "reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}",
"reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga", "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga",
"reassing_hint": "Seosta valitud üksused olemasoleva isikuga", "reassing_hint": "Seosta valitud üksused olemasoleva isikuga",
"recent_searches": "Hiljutised otsingud",
"refresh": "Värskenda", "refresh": "Värskenda",
"refresh_encoded_videos": "Värskenda kodeeritud videod", "refresh_encoded_videos": "Värskenda kodeeritud videod",
"refresh_metadata": "Värskenda metaandmed", "refresh_metadata": "Värskenda metaandmed",
@@ -862,14 +766,11 @@
"remove_assets_title": "Eemalda üksused?", "remove_assets_title": "Eemalda üksused?",
"remove_from_album": "Eemalda albumist", "remove_from_album": "Eemalda albumist",
"remove_from_favorites": "Eemalda lemmikutest", "remove_from_favorites": "Eemalda lemmikutest",
"remove_from_shared_link": "Eemalda jagatud lingist",
"remove_user": "Eemalda kasutaja", "remove_user": "Eemalda kasutaja",
"removed_api_key": "API võti eemaldatud: {name}", "removed_api_key": "API võti eemaldatud: {name}",
"removed_from_archive": "Arhiivist eemaldatud", "removed_from_archive": "Arhiivist eemaldatud",
"removed_from_favorites": "Lemmikutest eemaldatud", "removed_from_favorites": "Lemmikutest eemaldatud",
"removed_from_favorites_count": "{count, plural, other {# eemaldatud}} lemmikutest",
"removed_tagged_assets": "Silt eemaldatud {count, plural, one {# üksuselt} other {# üksuselt}}", "removed_tagged_assets": "Silt eemaldatud {count, plural, one {# üksuselt} other {# üksuselt}}",
"rename": "Nimeta ümber",
"require_password": "Nõua parooli", "require_password": "Nõua parooli",
"require_user_to_change_password_on_first_login": "Nõua kasutajalt esmakordsel sisenemisel parooli muutmist", "require_user_to_change_password_on_first_login": "Nõua kasutajalt esmakordsel sisenemisel parooli muutmist",
"reset": "Lähtesta", "reset": "Lähtesta",
@@ -905,10 +806,8 @@
"search_for_existing_person": "Otsi olemasolevat isikut", "search_for_existing_person": "Otsi olemasolevat isikut",
"search_no_people": "Isikuid ei ole", "search_no_people": "Isikuid ei ole",
"search_no_people_named": "Ei ole isikuid nimega \"{name}\"", "search_no_people_named": "Ei ole isikuid nimega \"{name}\"",
"search_options": "Otsingu valikud",
"search_people": "Otsi inimesi", "search_people": "Otsi inimesi",
"search_places": "Otsi kohti", "search_places": "Otsi kohti",
"search_settings": "Otsingu seaded",
"search_state": "Otsi osariiki...", "search_state": "Otsi osariiki...",
"search_tags": "Otsi silte...", "search_tags": "Otsi silte...",
"search_timezone": "Otsi ajavööndit...", "search_timezone": "Otsi ajavööndit...",
@@ -963,18 +862,13 @@
"show_metadata": "Kuva metaandmed", "show_metadata": "Kuva metaandmed",
"show_or_hide_info": "Kuva või peida info", "show_or_hide_info": "Kuva või peida info",
"show_password": "Kuva parooli", "show_password": "Kuva parooli",
"show_progress_bar": "Kuva edenemisriba",
"show_search_options": "Kuva otsingu valikud",
"show_supporter_badge": "Toetaja märk", "show_supporter_badge": "Toetaja märk",
"show_supporter_badge_description": "Kuva toetaja märki", "show_supporter_badge_description": "Kuva toetaja märki",
"sidebar": "Külgmenüü", "sidebar": "Külgmenüü",
"sidebar_display_description": "Kuva külgmenüüs linki vaatele",
"sign_out": "Logi välja", "sign_out": "Logi välja",
"sign_up": "Registreeru", "sign_up": "Registreeru",
"size": "Suurus", "size": "Suurus",
"skip_to_content": "Sisu juurde", "skip_to_content": "Sisu juurde",
"skip_to_folders": "Kaustade juurde",
"skip_to_tags": "Siltide juurde",
"slideshow": "Slaidiesitlus", "slideshow": "Slaidiesitlus",
"slideshow_settings": "Slaidiesitluse seaded", "slideshow_settings": "Slaidiesitluse seaded",
"sort_albums_by": "Järjesta albumid...", "sort_albums_by": "Järjesta albumid...",
@@ -1008,7 +902,6 @@
"theme": "Teema", "theme": "Teema",
"theme_selection": "Teema valik", "theme_selection": "Teema valik",
"theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele", "theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele",
"time_based_memories": "Ajapõhised mälestused",
"timezone": "Ajavöönd", "timezone": "Ajavöönd",
"to_archive": "Arhiivi", "to_archive": "Arhiivi",
"to_change_password": "Muuda parool", "to_change_password": "Muuda parool",
@@ -1024,8 +917,6 @@
"unnamed_album_delete_confirmation": "Kas oled kindel, et soovid selle albumi kustutada?", "unnamed_album_delete_confirmation": "Kas oled kindel, et soovid selle albumi kustutada?",
"unsaved_change": "Salvestamata muudatus", "unsaved_change": "Salvestamata muudatus",
"updated_password": "Parool muudetud", "updated_password": "Parool muudetud",
"upload": "Laadi üles",
"upload_concurrency": "Üleslaadimise samaaegsus",
"upload_errors": "Üleslaadimine lõpetatud {count, plural, one {# veaga} other {# veaga}}, uute üksuste nägemiseks värskenda lehte.", "upload_errors": "Üleslaadimine lõpetatud {count, plural, one {# veaga} other {# veaga}}, uute üksuste nägemiseks värskenda lehte.",
"upload_skipped_duplicates": "{count, plural, one {# dubleeritud üksus} other {# dubleeritud üksust}} vahele jäetud", "upload_skipped_duplicates": "{count, plural, one {# dubleeritud üksus} other {# dubleeritud üksust}} vahele jäetud",
"upload_status_duplicates": "Duplikaadid", "upload_status_duplicates": "Duplikaadid",
@@ -1036,7 +927,6 @@
"user": "Kasutaja", "user": "Kasutaja",
"user_id": "Kasutaja ID", "user_id": "Kasutaja ID",
"user_liked": "Kasutajale {user} meeldis {type, select, photo {see foto} video {see video} asset {see üksus} other {see}}", "user_liked": "Kasutajale {user} meeldis {type, select, photo {see foto} video {see video} asset {see üksus} other {see}}",
"user_purchase_settings": "Osta",
"user_purchase_settings_description": "Halda oma ostu", "user_purchase_settings_description": "Halda oma ostu",
"username": "Kasutajanimi", "username": "Kasutajanimi",
"users": "Kasutajad", "users": "Kasutajad",
@@ -1045,7 +935,6 @@
"variables": "Muutujad", "variables": "Muutujad",
"version": "Versioon", "version": "Versioon",
"version_announcement_closing": "Sinu sõber, Alex", "version_announcement_closing": "Sinu sõber, Alex",
"version_announcement_message": "Hei sõber, saadaval on rakenduse uus versioon. Palun võta aega, et lugeda <link>väljalasketeadet</link> ning veendu, et su <code>docker-compose.yml</code> ja <code>.env</code> failid on ajakohased, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it või muud mehhanismi, mis rakendust automaatselt uuendab.",
"video": "Video", "video": "Video",
"video_hover_setting": "Esita hõljutamisel video eelvaade", "video_hover_setting": "Esita hõljutamisel video eelvaade",
"video_hover_setting_description": "Esita video eelvaade, kui hiirt selle kohal hõljutada. Isegi kui keelatud, saab taasesituse alustada taasesitusnupu kohal hõljutades.", "video_hover_setting_description": "Esita video eelvaade, kui hiirt selle kohal hõljutada. Isegi kui keelatud, saab taasesituse alustada taasesitusnupu kohal hõljutades.",
+3 -3
View File
@@ -194,7 +194,7 @@
"refreshing_all_libraries": "بروز رسانی همه کتابخانه ها", "refreshing_all_libraries": "بروز رسانی همه کتابخانه ها",
"registration": "ثبت نام مدیر", "registration": "ثبت نام مدیر",
"registration_description": "از آنجایی که شما اولین کاربر در سیستم هستید، به عنوان مدیر تعیین شده‌اید و مسئولیت انجام وظایف مدیریتی بر عهده شما خواهد بود و کاربران اضافی توسط شما ایجاد خواهند شد.", "registration_description": "از آنجایی که شما اولین کاربر در سیستم هستید، به عنوان مدیر تعیین شده‌اید و مسئولیت انجام وظایف مدیریتی بر عهده شما خواهد بود و کاربران اضافی توسط شما ایجاد خواهند شد.",
"removing_deleted_files": "حذف فایل‌های آفلاین", "removing_offline_files": "حذف فایل‌های آفلاین",
"repair_all": "بازسازی همه", "repair_all": "بازسازی همه",
"repair_matched_items": "", "repair_matched_items": "",
"repaired_items": "", "repaired_items": "",
@@ -524,8 +524,8 @@
"unable_to_refresh_user": "", "unable_to_refresh_user": "",
"unable_to_remove_album_users": "", "unable_to_remove_album_users": "",
"unable_to_remove_api_key": "", "unable_to_remove_api_key": "",
"unable_to_remove_deleted_assets": "",
"unable_to_remove_library": "", "unable_to_remove_library": "",
"unable_to_remove_offline_files": "",
"unable_to_remove_partner": "", "unable_to_remove_partner": "",
"unable_to_remove_reaction": "", "unable_to_remove_reaction": "",
"unable_to_repair_items": "", "unable_to_repair_items": "",
@@ -766,10 +766,10 @@
"refreshed": "", "refreshed": "",
"refreshes_every_file": "", "refreshes_every_file": "",
"remove": "", "remove": "",
"remove_deleted_assets": "",
"remove_from_album": "", "remove_from_album": "",
"remove_from_favorites": "", "remove_from_favorites": "",
"remove_from_shared_link": "", "remove_from_shared_link": "",
"remove_offline_files": "",
"removed_api_key": "", "removed_api_key": "",
"rename": "", "rename": "",
"repair": "", "repair": "",
+119 -324
View File
@@ -25,7 +25,7 @@
"add_to_shared_album": "Lisää jaettuun albumiin", "add_to_shared_album": "Lisää jaettuun albumiin",
"added_to_archive": "Arkistoitu", "added_to_archive": "Arkistoitu",
"added_to_favorites": "Lisätty suosikkeihin", "added_to_favorites": "Lisätty suosikkeihin",
"added_to_favorites_count": "{count, number} lisätty suosikkeihin", "added_to_favorites_count": "{count} lisätty suosikkeihin",
"admin": { "admin": {
"add_exclusion_pattern_description": "Lisää mallit, jonka mukaan jätetään tiedostoja pois. Jokerimerkit *, ** ja ? ovat tuettuna. Jättääksesi pois kaikki tiedostot mistä tahansa löytyvästä kansiosta \"Raw\" käytä \"**/Raw/**\". Jättääksesi pois kaikki \". tif\" päätteiset tiedot, käytä \"**/*.tif\". Jättääksesi pois tarkan tiedostopolun, käytä \"/path/to/ignore/**\".", "add_exclusion_pattern_description": "Lisää mallit, jonka mukaan jätetään tiedostoja pois. Jokerimerkit *, ** ja ? ovat tuettuna. Jättääksesi pois kaikki tiedostot mistä tahansa löytyvästä kansiosta \"Raw\" käytä \"**/Raw/**\". Jättääksesi pois kaikki \". tif\" päätteiset tiedot, käytä \"**/*.tif\". Jättääksesi pois tarkan tiedostopolun, käytä \"/path/to/ignore/**\".",
"authentication_settings": "Autentikointiasetukset", "authentication_settings": "Autentikointiasetukset",
@@ -41,7 +41,6 @@
"confirm_email_below": "Kirjota \"{email}\" vahvistaaksesi", "confirm_email_below": "Kirjota \"{email}\" vahvistaaksesi",
"confirm_reprocess_all_faces": "Haluatko varmasti käsitellä uudelleen kaikki kasvot? Tämä poistaa myös nimetyt henkilöt.", "confirm_reprocess_all_faces": "Haluatko varmasti käsitellä uudelleen kaikki kasvot? Tämä poistaa myös nimetyt henkilöt.",
"confirm_user_password_reset": "Haluatko varmasti nollata käyttäjän {user} salasanan?", "confirm_user_password_reset": "Haluatko varmasti nollata käyttäjän {user} salasanan?",
"create_job": "Luo tehtävä",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "Poista kirjautuminen käytöstä", "disable_login": "Poista kirjautuminen käytöstä",
"disabled": "Ei käytössä", "disabled": "Ei käytössä",
@@ -71,13 +70,12 @@
"image_thumbnail_resolution": "Pikkukuvien resoluutio", "image_thumbnail_resolution": "Pikkukuvien resoluutio",
"image_thumbnail_resolution_description": "Käytetään katsottaessa useita kuvia kerralla (aikajana, albuminäkymä, jne.) Korkeampi resoluutio antaa enemmän yksityiskohtia, mutta niiden luonti kestää kauemmin, tiedostokoot ovat isompia ja voivat heikentää sovelluksen responsiivisuutta.", "image_thumbnail_resolution_description": "Käytetään katsottaessa useita kuvia kerralla (aikajana, albuminäkymä, jne.) Korkeampi resoluutio antaa enemmän yksityiskohtia, mutta niiden luonti kestää kauemmin, tiedostokoot ovat isompia ja voivat heikentää sovelluksen responsiivisuutta.",
"job_concurrency": "{job} yhtäaikaisuus", "job_concurrency": "{job} yhtäaikaisuus",
"job_created": "Tehtävä luotu",
"job_not_concurrency_safe": "Tätä tehtävää ei ole turvallista ajaa yhtäaikaisesti.", "job_not_concurrency_safe": "Tätä tehtävää ei ole turvallista ajaa yhtäaikaisesti.",
"job_settings": "Tehtävän asetukset", "job_settings": "Tehtävän asetukset",
"job_settings_description": "Hallitse tehtävän samanaikaisuusasetuksia", "job_settings_description": "Hallitse tehtävän samanaikaisuusasetuksia",
"job_status": "Tehtävän tila", "job_status": "Tehtävän tila",
"jobs_delayed": "{jobCount, plural, other {# viivästynyttä}}", "jobs_delayed": "{jobCount} tehtävää viivästetty",
"jobs_failed": "{jobCount, plural, other {# epäonnistunutta}}", "jobs_failed": "{jobCount} epäonnistui",
"library_created": "Kirjasto {library} luotu", "library_created": "Kirjasto {library} luotu",
"library_cron_expression": "Cron-lauseke", "library_cron_expression": "Cron-lauseke",
"library_cron_expression_description": "Anna skannaustiheys cron-formaatissa. Saadaksesi lisätietoja katso esimerkiksi <link>Crontab Guru</link>", "library_cron_expression_description": "Anna skannaustiheys cron-formaatissa. Saadaksesi lisätietoja katso esimerkiksi <link>Crontab Guru</link>",
@@ -137,13 +135,13 @@
"map_reverse_geocoding": "Käänteinen Geokoodaus", "map_reverse_geocoding": "Käänteinen Geokoodaus",
"map_reverse_geocoding_enable_description": "Ota käyttöön osoitteiden poiminta karttakoordinaateista", "map_reverse_geocoding_enable_description": "Ota käyttöön osoitteiden poiminta karttakoordinaateista",
"map_reverse_geocoding_settings": "Käänteisen Geokoodauksen asetukset", "map_reverse_geocoding_settings": "Käänteisen Geokoodauksen asetukset",
"map_settings": "Kartta", "map_settings": "Kartta-asetukset",
"map_settings_description": "Hallitse kartan asetuksia", "map_settings_description": "Hallitse kartan asetuksia",
"map_style_description": "style.json -karttateeman URL", "map_style_description": "style.json -karttateeman URL",
"metadata_extraction_job": "Kerää metadata", "metadata_extraction_job": "Kerää metadata",
"metadata_extraction_job_description": "Poimi metatiedot aineistoista, kuten GPS, kasvot ja resoluutio", "metadata_extraction_job_description": "Poimi metatiedot aineistoista, kuten GPS ja resoluutio",
"metadata_faces_import_setting": "Ota käyttöön kasvojen tuonti", "metadata_faces_import_setting": "Ota käyttöön kasvojen tuonti",
"metadata_faces_import_setting_description": "Tuo kasvot kuvan EXIF- ja kylkiäistiedostoista", "metadata_faces_import_setting_description": "Tuo kasvot kuvan EXIF -tiedoista ja kylkiäistiedostoista",
"metadata_settings": "Metatietoasetukset", "metadata_settings": "Metatietoasetukset",
"metadata_settings_description": "Hallitse metatietoja", "metadata_settings_description": "Hallitse metatietoja",
"migration_job": "Migrointi", "migration_job": "Migrointi",
@@ -180,9 +178,9 @@
"oauth_issuer_url": "Toimitsijan URL", "oauth_issuer_url": "Toimitsijan URL",
"oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI", "oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI",
"oauth_mobile_redirect_uri_override": "Ohita mobiilin uudelleenohjaus-URI", "oauth_mobile_redirect_uri_override": "Ohita mobiilin uudelleenohjaus-URI",
"oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun OAuth tarjoaja ei salli mobiili URI:a, kuten '{callback}'", "oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun 'app.immich:/' -ohjausta ei tueta.",
"oauth_profile_signing_algorithm": "Profiilin allekirjoitusalgoritmi", "oauth_profile_signing_algorithm": "Profiilin allekirjoitusalgoritmi",
"oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoittamiseen.", "oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoituksessa",
"oauth_scope": "Skooppi (Scope)", "oauth_scope": "Skooppi (Scope)",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Hallitse OAuth kirjautumisen asetuksia", "oauth_settings_description": "Hallitse OAuth kirjautumisen asetuksia",
@@ -200,12 +198,11 @@
"password_settings": "Kirjaudu salasanalla", "password_settings": "Kirjaudu salasanalla",
"password_settings_description": "Hallitse salasanakirjautumisen asetuksia", "password_settings_description": "Hallitse salasanakirjautumisen asetuksia",
"paths_validated_successfully": "Kaikki polut validoitu", "paths_validated_successfully": "Kaikki polut validoitu",
"person_cleanup_job": "Henkilöpuhdistus",
"quota_size_gib": "Kiintiön koko (Gt)", "quota_size_gib": "Kiintiön koko (Gt)",
"refreshing_all_libraries": "Virkistetään kaikki kirjastot", "refreshing_all_libraries": "Virkistetään kaikki kirjastot",
"registration": "Pääkäyttäjän rekisteröinti", "registration": "Pääkäyttäjän rekisteröinti",
"registration_description": "Pääkäyttäjänä olet vastuussa järjestelmän hallinnallisista tehtävistä ja uusien käyttäjien luomisesta.", "registration_description": "Pääkäyttäjänä olet vastuussa järjestelmän hallinnallisista tehtävistä ja uusien käyttäjien luomisesta.",
"removing_deleted_files": "Poistetaan Offline-tiedostot", "removing_offline_files": "Poistetaan Offline-tiedostot",
"repair_all": "Korjaa kaikki", "repair_all": "Korjaa kaikki",
"repair_matched_items": "Löytyi {count, plural, one {# osuma} other {# osumaa}}", "repair_matched_items": "Löytyi {count, plural, one {# osuma} other {# osumaa}}",
"repaired_items": "Korjattiin {count, plural, one {# kohta} other {# kohtaa}}", "repaired_items": "Korjattiin {count, plural, one {# kohta} other {# kohtaa}}",
@@ -214,7 +211,6 @@
"reset_settings_to_recent_saved": "Palauta aiemmin tallennetut asetukset", "reset_settings_to_recent_saved": "Palauta aiemmin tallennetut asetukset",
"scanning_library_for_changed_files": "Etsitään kirjaston muuttuneita tiedostoja", "scanning_library_for_changed_files": "Etsitään kirjaston muuttuneita tiedostoja",
"scanning_library_for_new_files": "Etsitään uusia tiedostoja", "scanning_library_for_new_files": "Etsitään uusia tiedostoja",
"search_jobs": "Etsi tehtäviä...",
"send_welcome_email": "Lähetä tervetuloviesti", "send_welcome_email": "Lähetä tervetuloviesti",
"server_external_domain_settings": "Ulkoinen osoite", "server_external_domain_settings": "Ulkoinen osoite",
"server_external_domain_settings_description": "Osoite julkisille linkeille, http(s):// mukaan lukien", "server_external_domain_settings_description": "Osoite julkisille linkeille, http(s):// mukaan lukien",
@@ -242,7 +238,6 @@
"storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä", "storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä",
"storage_template_user_label": "<code>{label}</code> on käyttäjän Tallennustilan Tunniste", "storage_template_user_label": "<code>{label}</code> on käyttäjän Tallennustilan Tunniste",
"system_settings": "Järjestelmäasetukset", "system_settings": "Järjestelmäasetukset",
"tag_cleanup_job": "Merkintäpuhdistus",
"theme_custom_css_settings": "Mukautettu CSS", "theme_custom_css_settings": "Mukautettu CSS",
"theme_custom_css_settings_description": "Kustomoi Immichin ulkoasua Cascading Style Sheets:llä.", "theme_custom_css_settings_description": "Kustomoi Immichin ulkoasua Cascading Style Sheets:llä.",
"theme_settings": "Teeman asetukset", "theme_settings": "Teeman asetukset",
@@ -270,7 +265,7 @@
"transcoding_codecs_learn_more": "Oppiaksesi lisää tässä käytetystä terminologiasta, tutustu FFmpeg- dokumentaatioon <h264-link>H.264 koodaaja</h264-link>, <hevc-link>HEVC koodaaja</hevc-link> sekä <vp9-link>VP9 koodaaja</vp9-link>.", "transcoding_codecs_learn_more": "Oppiaksesi lisää tässä käytetystä terminologiasta, tutustu FFmpeg- dokumentaatioon <h264-link>H.264 koodaaja</h264-link>, <hevc-link>HEVC koodaaja</hevc-link> sekä <vp9-link>VP9 koodaaja</vp9-link>.",
"transcoding_constant_quality_mode": "Tasaisen laadun tyyppi", "transcoding_constant_quality_mode": "Tasaisen laadun tyyppi",
"transcoding_constant_quality_mode_description": "ICQ on parempi kuin CQP, mutta jotkut laitteistokiihdyttimet eivät tue sitä. Tätä asetusta käytetään oletuksena laatuun pohjautuvissa muunnoksissa, paitsi NVENC mikä ei tue ICQ:ta.", "transcoding_constant_quality_mode_description": "ICQ on parempi kuin CQP, mutta jotkut laitteistokiihdyttimet eivät tue sitä. Tätä asetusta käytetään oletuksena laatuun pohjautuvissa muunnoksissa, paitsi NVENC mikä ei tue ICQ:ta.",
"transcoding_constant_rate_factor": "Vakionopeustekijä", "transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "Videon laatu. Yleisimmät arvot ovat 23 H.264:lle, 28 HEVC:lle, 31 VP9:lle ja 35 AV1:lle. Matalampi arvo on parempi, mutta tekee isompia tiedostoja.", "transcoding_constant_rate_factor_description": "Videon laatu. Yleisimmät arvot ovat 23 H.264:lle, 28 HEVC:lle, 31 VP9:lle ja 35 AV1:lle. Matalampi arvo on parempi, mutta tekee isompia tiedostoja.",
"transcoding_disabled_description": "Älä muunna videoita. Voi joissakin päätelaitteissa aiheuttaa videotoiston toimimattomuutta", "transcoding_disabled_description": "Älä muunna videoita. Voi joissakin päätelaitteissa aiheuttaa videotoiston toimimattomuutta",
"transcoding_hardware_acceleration": "Laitteistokiihdytys", "transcoding_hardware_acceleration": "Laitteistokiihdytys",
@@ -288,7 +283,7 @@
"transcoding_preferred_hardware_device": "Ensisijainen laite", "transcoding_preferred_hardware_device": "Ensisijainen laite",
"transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.", "transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.",
"transcoding_preset_preset": "Esiasetus (-asetus)", "transcoding_preset_preset": "Esiasetus (-asetus)",
"transcoding_preset_preset_description": "Pakkausnopeus. Hitaampi tuottaa pienempiä tiedostoja ja parantaa laatua, kun kohdistetaan tiettyyn bittinopeuteen. VP9 ei huomioi korkeampaa kuin 'faster'.", "transcoding_preset_preset_description": "Pakkausnopeus. Hitaampi tuottaa pienempiä tiedostoja ja parantaa laatua, kun kohdistetaan tiettyyn bittinopeuteen. VP9 ei huomioi korkeampaa kuin `faster`.",
"transcoding_reference_frames": "Kehysviitteet", "transcoding_reference_frames": "Kehysviitteet",
"transcoding_reference_frames_description": "Viittaavien kehysten määrä kun tiettyä kehystä pakataan. Korkeampi arvo parantaa pakkausta mutta hidastaa enkoodausta. 0 määrittää arvon automaattisesti.", "transcoding_reference_frames_description": "Viittaavien kehysten määrä kun tiettyä kehystä pakataan. Korkeampi arvo parantaa pakkausta mutta hidastaa enkoodausta. 0 määrittää arvon automaattisesti.",
"transcoding_required_description": "Vain videoille, jotka eivät ole hyväksytyssä muodossa", "transcoding_required_description": "Vain videoille, jotka eivät ole hyväksytyssä muodossa",
@@ -307,7 +302,7 @@
"transcoding_transcode_policy": "Transkoodauskäytäntö", "transcoding_transcode_policy": "Transkoodauskäytäntö",
"transcoding_transcode_policy_description": "Käytäntö miten video tulisi transkoodata. HDR videot transkoodataan aina, paitsi jos transkoodaus on poistettu käytöstä.", "transcoding_transcode_policy_description": "Käytäntö miten video tulisi transkoodata. HDR videot transkoodataan aina, paitsi jos transkoodaus on poistettu käytöstä.",
"transcoding_two_pass_encoding": "Two-pass enkoodaus", "transcoding_two_pass_encoding": "Two-pass enkoodaus",
"transcoding_two_pass_encoding_setting_description": "Transkoodaa kahdessa vaiheessa tuottaaksesi paremmin koodattuja videoita. Kun maksimibittinopeus on käytössä (vaaditaan H.264- ja HEVC-koodaukselle), tämä tila käyttää bittinopeusaluetta, joka perustuu maksimibittinopeuteen ja ohittaa CRF. VP9 osalta CRF:ää voidaan käyttää, jos maksimibittinopeus on poistettu käytöstä.", "transcoding_two_pass_encoding_setting_description": "",
"transcoding_video_codec": "Videokoodekki", "transcoding_video_codec": "Videokoodekki",
"transcoding_video_codec_description": "VP9 on tehokkain ja web-yhteensopiva, mutta muuntaminen kestää kauemmin. HEVC suoriutuu yhtäläisesti, mutta ei ole ihan yhtä yhteensopiva. H.264 on hyvin yhteensopiva ja nopea muuntaa, mutta tuottaa paljon suurempia tiedostoja. AV1 on kaikkein tehokkain koodekki, mutta vanhemmat laitteet eivät sitä tue.", "transcoding_video_codec_description": "VP9 on tehokkain ja web-yhteensopiva, mutta muuntaminen kestää kauemmin. HEVC suoriutuu yhtäläisesti, mutta ei ole ihan yhtä yhteensopiva. H.264 on hyvin yhteensopiva ja nopea muuntaa, mutta tuottaa paljon suurempia tiedostoja. AV1 on kaikkein tehokkain koodekki, mutta vanhemmat laitteet eivät sitä tue.",
"trash_enabled_description": "Ota käyttöön roskakori", "trash_enabled_description": "Ota käyttöön roskakori",
@@ -317,22 +312,15 @@
"trash_settings_description": "Hallitse roskakoriasetuksia", "trash_settings_description": "Hallitse roskakoriasetuksia",
"untracked_files": "Tiedostot joita ei seurata", "untracked_files": "Tiedostot joita ei seurata",
"untracked_files_description": "Nämä tiedostot eivät ole ohjelman hallitsemia. Ne voivat olla virheellisten siirtojen tai keskeytyneiden latausten tulosta, tai bugista johtuvia jälkeen jääneitä", "untracked_files_description": "Nämä tiedostot eivät ole ohjelman hallitsemia. Ne voivat olla virheellisten siirtojen tai keskeytyneiden latausten tulosta, tai bugista johtuvia jälkeen jääneitä",
"user_cleanup_job": "Käyttäjien puhdistus",
"user_delete_delay": "Käyttäjän <b>{user}</b> tili ja aineistot aikataulutetaan poistettavaksi ajan kuluttua: {delay, plural, one {# day} other {# days}}.",
"user_delete_delay_settings": "Poiston viive", "user_delete_delay_settings": "Poiston viive",
"user_delete_delay_settings_description": "Montako päivää poistamisen jälkeen käyttäjä ja hänen aineistonsa poistetaan pysyvästi. Joka keskiyö käydään läpi poistetuiksi merkityt käyttäjät. Tämä muutos astuu voimaan seuraavalla ajokerralla.", "user_delete_delay_settings_description": "Montako päivää poistamisen jälkeen käyttäjä ja hänen aineistonsa poistetaan pysyvästi. Joka keskiyö käydään läpi poistetuiksi merkityt käyttäjät. Tämä muutos astuu voimaan seuraavalla ajokerralla.",
"user_delete_immediately": "<b>{user}</b>:n tili ja sen kohteet on ajastettu poistettavaksi <b>heti</b>.",
"user_delete_immediately_checkbox": "Aseta tili ja sen kohteet jonoon välitöntä poistoa varten",
"user_management": "Käyttäjien hallinta", "user_management": "Käyttäjien hallinta",
"user_password_has_been_reset": "Käyttäjän salasana on nollattu:", "user_password_has_been_reset": "Käyttäjän salasana on nollattu:",
"user_password_reset_description": "Anna väliaikainen salasana ja ohjeista käyttäjää vaihtamaan se seuraavan kirjautumisen yhteydessä.", "user_password_reset_description": "Anna väliaikainen salasana ja ohjeista käyttäjää vaihtamaan se seuraavan kirjautumisen yhteydessä.",
"user_restore_description": "<b>{user}</b>:n tili palautetaan.",
"user_restore_scheduled_removal": "Palauta käyttäjä - Aikataulutettu poisto tapahtuu {date, date, long}",
"user_settings": "Käyttäjäasetukset", "user_settings": "Käyttäjäasetukset",
"user_settings_description": "Hallitse käyttäjäasetuksia", "user_settings_description": "Hallitse käyttäjäasetuksia",
"user_successfully_removed": "Käyttäjä {email} on poistettu.", "user_successfully_removed": "Käyttäjä {email} on poistettu.",
"version_check_enabled_description": "Ota käyttöön versiotarkastus", "version_check_enabled_description": "Ota käyttöön säännölliset uusien versioiden tarkistukset GitHubista",
"version_check_implications": "Versiontarkistus vaatii säännöllisen yhteyden github.com:iin",
"version_check_settings": "Versiotarkistus", "version_check_settings": "Versiotarkistus",
"version_check_settings_description": "Ota käyttöön ilmoitukset, kun uusi versio on saatavilla", "version_check_settings_description": "Ota käyttöön ilmoitukset, kun uusi versio on saatavilla",
"video_conversion_job": "Transkoodaa videot", "video_conversion_job": "Transkoodaa videot",
@@ -348,21 +336,17 @@
"album_added": "Albumi lisätty", "album_added": "Albumi lisätty",
"album_added_notification_setting_description": "Saa sähköpostia kun sinut lisätään jaettuun albumiin", "album_added_notification_setting_description": "Saa sähköpostia kun sinut lisätään jaettuun albumiin",
"album_cover_updated": "Albumin kansikuva päivitetty", "album_cover_updated": "Albumin kansikuva päivitetty",
"album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?", "album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?\nJos albumi on jaettu, muut eivät pääse siihen enää.",
"album_delete_confirmation_description": "Jos albumi on jaettu, muut eivät pääse siihen enää.",
"album_info_updated": "Albumin tiedot päivitetty", "album_info_updated": "Albumin tiedot päivitetty",
"album_leave": "Poistu albumista?", "album_leave": "Poistu albumista?",
"album_leave_confirmation": "Haluatko varmasti poistua albumista {album}?",
"album_name": "Albumin nimi", "album_name": "Albumin nimi",
"album_options": "Albumin asetukset", "album_options": "Albumin asetukset",
"album_remove_user": "Poista käyttäjä?", "album_remove_user": "Poista käyttäjä?",
"album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?",
"album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.", "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.",
"album_updated": "Albumi päivitetty", "album_updated": "Albumi päivitetty",
"album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä", "album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä",
"album_user_left": "Poistuttiin albumista {album}",
"album_user_removed": "{user} poistettu", "album_user_removed": "{user} poistettu",
"album_with_link_access": "Anna kenen tahansa nähdä linkin kautta tämän albumin valokuvat ja henkilöt.",
"albums": "Albumit", "albums": "Albumit",
"albums_count": "{count, plural, one {{count, number} albumi} other {{count, number} albumia}}", "albums_count": "{count, plural, one {{count, number} albumi} other {{count, number} albumia}}",
"all": "Kaikki", "all": "Kaikki",
@@ -371,12 +355,7 @@
"all_videos": "Kaikki videot", "all_videos": "Kaikki videot",
"allow_dark_mode": "Salli tumma tila", "allow_dark_mode": "Salli tumma tila",
"allow_edits": "Salli muutokset", "allow_edits": "Salli muutokset",
"allow_public_user_to_download": "Salli julkisten käyttäjien ladata tiedostoja",
"allow_public_user_to_upload": "Salli julkisten käyttäjien lähettää tiedostoja",
"anti_clockwise": "Vastapäivään",
"api_key": "API-avain", "api_key": "API-avain",
"api_key_description": "Tämä arvo näytetään vain kerran. Varmista, että olet kopioinut sen ennen kuin suljet ikkunan.",
"api_key_empty": "API-avaimesi ei pitäisi olla tyhjä",
"api_keys": "API-avaimet", "api_keys": "API-avaimet",
"app_settings": "Sovellusasetukset", "app_settings": "Sovellusasetukset",
"appears_in": "Esiintyy albumeissa", "appears_in": "Esiintyy albumeissa",
@@ -390,20 +369,14 @@
"are_you_sure_to_do_this": "Haluatko varmasti tehdä tämän?", "are_you_sure_to_do_this": "Haluatko varmasti tehdä tämän?",
"asset_added_to_album": "Lisätty albumiin", "asset_added_to_album": "Lisätty albumiin",
"asset_adding_to_album": "Lisätään albumiin...", "asset_adding_to_album": "Lisätään albumiin...",
"asset_description_updated": "Kohteen kuvaus on päivitetty",
"asset_filename_is_offline": "Kohde {filename} on offline-tilassa",
"asset_has_unassigned_faces": "Kohteella on määrittämättömiä kasvoja",
"asset_hashing": "Hajautetaan...",
"asset_offline": "Aineisto offline-tilassa", "asset_offline": "Aineisto offline-tilassa",
"asset_offline_description": "Tämä kohde on offline-tilassa. Immich ei pääse tiedoston sijaintiin. Varmista, että kohde on saatavilla, ja skannaa sitten kirjasto uudelleen.",
"asset_skipped": "Ohitettu", "asset_skipped": "Ohitettu",
"asset_skipped_in_trash": "Roskakorissa",
"asset_uploaded": "Lähetetty", "asset_uploaded": "Lähetetty",
"asset_uploading": "Lähetetään…", "asset_uploading": "Lähetetään…",
"assets": "kohdetta", "assets": "kohdetta",
"assets_added_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}}", "assets_added_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}}",
"assets_added_to_album_count": "Albumiin lisätty {count, plural, one {# kohde} other {# kohdetta}}", "assets_added_to_album_count": "Albumiin lisätty {count, plural, one {# kohde} other {# kohdetta}}",
"assets_added_to_name_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}} {hasName, select, true {<b>{name}</b>} other {uuteen albumiin}}", "assets_added_to_name_count": "{name}:n lisätty {count, plural, one {# media} other {# mediaa}}",
"assets_count": "{count, plural, one {# media} other {# mediaa}}", "assets_count": "{count, plural, one {# media} other {# mediaa}}",
"assets_moved_to_trash": "Siirretty {count, plural, one {# aineisto} other {# aineistoa}} roskakoriin", "assets_moved_to_trash": "Siirretty {count, plural, one {# aineisto} other {# aineistoa}} roskakoriin",
"assets_moved_to_trash_count": "Siirretty {count, plural, one {# media} other {# mediaa}} roskakoriin", "assets_moved_to_trash_count": "Siirretty {count, plural, one {# media} other {# mediaa}} roskakoriin",
@@ -425,7 +398,6 @@
"bulk_delete_duplicates_confirmation": "Haluatko varmasti poistaa {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} kerralla? Tämä säilyttää kustakin mediasta kookkaimman ja poistaa loput pysyvästi. Et voi perua tätä!", "bulk_delete_duplicates_confirmation": "Haluatko varmasti poistaa {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} kerralla? Tämä säilyttää kustakin mediasta kookkaimman ja poistaa loput pysyvästi. Et voi perua tätä!",
"bulk_keep_duplicates_confirmation": "Haluatko varmasti säilyttää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}}? Tämä merkitsee kaikki kaksoiskappaleet ratkaistuiksi, eikä poista mitään.", "bulk_keep_duplicates_confirmation": "Haluatko varmasti säilyttää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}}? Tämä merkitsee kaikki kaksoiskappaleet ratkaistuiksi, eikä poista mitään.",
"bulk_trash_duplicates_confirmation": "Haluatko varmasti siirtää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} roskakoriin? Tämä säilyttää kustakin mediasta kookkaimman ja siirtää loput roskakoriin.", "bulk_trash_duplicates_confirmation": "Haluatko varmasti siirtää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} roskakoriin? Tämä säilyttää kustakin mediasta kookkaimman ja siirtää loput roskakoriin.",
"buy": "Osta lisenssi Immich:iin",
"camera": "Kamera", "camera": "Kamera",
"camera_brand": "Kameran merkki", "camera_brand": "Kameran merkki",
"camera_model": "Kameran malli", "camera_model": "Kameran malli",
@@ -443,7 +415,7 @@
"change_location": "Vaihda sijainti", "change_location": "Vaihda sijainti",
"change_name": "Vaihda nimi", "change_name": "Vaihda nimi",
"change_name_successfully": "Nimi vaihdettu", "change_name_successfully": "Nimi vaihdettu",
"change_password": "Vaihda Salasana", "change_password": "Vaihda salasana",
"change_password_description": "Tämä on joko ensimmäinen kertasi kun kirjaudut järjestelmään, tai salasanasi on pyydetty vaihtamaan. Määritä uusi salasana alle.", "change_password_description": "Tämä on joko ensimmäinen kertasi kun kirjaudut järjestelmään, tai salasanasi on pyydetty vaihtamaan. Määritä uusi salasana alle.",
"change_your_password": "Vaihda salasanasi", "change_your_password": "Vaihda salasanasi",
"changed_visibility_successfully": "Näkyvyys vaihdettu", "changed_visibility_successfully": "Näkyvyys vaihdettu",
@@ -453,14 +425,11 @@
"city": "Kaupunki", "city": "Kaupunki",
"clear": "Tyhjennä", "clear": "Tyhjennä",
"clear_all": "Tyhjennä kaikki", "clear_all": "Tyhjennä kaikki",
"clear_all_recent_searches": "Tyhjennä viimeisimmät haut",
"clear_message": "Tyhjennä viesti", "clear_message": "Tyhjennä viesti",
"clear_value": "Tyhjää arvo", "clear_value": "Tyhjää arvo",
"clockwise": "Myötäpäivään",
"close": "Sulje", "close": "Sulje",
"collapse": "Supista", "collapse": "Supista",
"collapse_all": "Sulje kaikki", "collapse_all": "Sulje kaikki",
"color": "Väri",
"color_theme": "Väriteema", "color_theme": "Väriteema",
"comment_deleted": "Kommentti poistettu", "comment_deleted": "Kommentti poistettu",
"comment_options": "Kommentin valinnat", "comment_options": "Kommentin valinnat",
@@ -494,15 +463,13 @@
"create_new_person": "Luo uusi henkilö", "create_new_person": "Luo uusi henkilö",
"create_new_person_hint": "Määritä valitut mediat uudelle henkilölle", "create_new_person_hint": "Määritä valitut mediat uudelle henkilölle",
"create_new_user": "Luo uusi käyttäjä", "create_new_user": "Luo uusi käyttäjä",
"create_tag": "Luo tunniste",
"create_tag_description": "Luo uusi tunniste. Sisäkkäisiä tunnisteita varten, syötä tunnisteen täydellinen polku kauttaviiva mukaanluettuna.",
"create_user": "Luo käyttäjä", "create_user": "Luo käyttäjä",
"created": "Luotu", "created": "Luotu",
"current_device": "Nykyinen laite", "current_device": "Nykyinen laite",
"custom_locale": "Muokatut maa-asetukset", "custom_locale": "Muokatut maa-asetukset",
"custom_locale_description": "Muotoile päivämäärät ja numerot perustuen alueen kieleen", "custom_locale_description": "Muotoile päivämäärät ja numerot perustuen alueen kieleen",
"dark": "Tumma", "dark": "Tumma",
"date_after": "Päivämäärän jälkeen", "date_after": "Päivä jälkeen",
"date_and_time": "Päivämäärä ja aika", "date_and_time": "Päivämäärä ja aika",
"date_before": "Päivä ennen", "date_before": "Päivä ennen",
"date_of_birth_saved": "Syntymäaika tallennettu", "date_of_birth_saved": "Syntymäaika tallennettu",
@@ -519,8 +486,6 @@
"delete_library": "Poista kirjasto", "delete_library": "Poista kirjasto",
"delete_link": "Poista linkki", "delete_link": "Poista linkki",
"delete_shared_link": "Poista jaettu linkki", "delete_shared_link": "Poista jaettu linkki",
"delete_tag": "Poista tunniste",
"delete_tag_confirmation_prompt": "Haluatko varmasti poistaa {tagName}-tunnisteen?",
"delete_user": "Poista käyttäjä", "delete_user": "Poista käyttäjä",
"deleted_shared_link": "Jaettu linkki poistettu", "deleted_shared_link": "Jaettu linkki poistettu",
"description": "Kuvaus", "description": "Kuvaus",
@@ -538,8 +503,6 @@
"do_not_show_again": "Älä näytä tätä enää", "do_not_show_again": "Älä näytä tätä enää",
"done": "Valmis", "done": "Valmis",
"download": "Lataa", "download": "Lataa",
"download_include_embedded_motion_videos": "Upotetut videot",
"download_include_embedded_motion_videos_description": "Sisällytä liikekuviin upotetut videot erillisinä tiedostoina",
"download_settings": "Lataukset", "download_settings": "Lataukset",
"download_settings_description": "Hallitse aineiston lataukseen liittyviä asetuksia", "download_settings_description": "Hallitse aineiston lataukseen liittyviä asetuksia",
"downloading": "Ladataan", "downloading": "Ladataan",
@@ -569,15 +532,10 @@
"edit_location": "Muokkaa sijaintia", "edit_location": "Muokkaa sijaintia",
"edit_name": "Muokkaa nimeä", "edit_name": "Muokkaa nimeä",
"edit_people": "Muokkaa henkilöitä", "edit_people": "Muokkaa henkilöitä",
"edit_tag": "Muokkaa tunnistetta",
"edit_title": "Muokkaa otsikkoa", "edit_title": "Muokkaa otsikkoa",
"edit_user": "Muokkaa käyttäjää", "edit_user": "Muokkaa käyttäjää",
"edited": "Muokattu", "edited": "Muokattu",
"editor": "Editori", "editor": "",
"editor_close_without_save_prompt": "Muutoksia ei tallenneta",
"editor_close_without_save_title": "Suljetaanko editori?",
"editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet",
"editor_crop_tool_h2_rotation": "Rotaatio",
"email": "Sähköposti", "email": "Sähköposti",
"empty": "", "empty": "",
"empty_album": "", "empty_album": "",
@@ -605,7 +563,6 @@
"error_adding_users_to_album": "Käyttäjiä ei voitu lisätä albumiin", "error_adding_users_to_album": "Käyttäjiä ei voitu lisätä albumiin",
"error_deleting_shared_user": "Jaettua käyttäjää ei voitu poistaa", "error_deleting_shared_user": "Jaettua käyttäjää ei voitu poistaa",
"error_downloading": "Tiedostoa {filename} ei voitu ladata", "error_downloading": "Tiedostoa {filename} ei voitu ladata",
"error_hiding_buy_button": "Virhe osta-painikkeen piilottamisessa",
"error_removing_assets_from_album": "Medioiden poisto epäonnistui. Katso konsolista lisätietoja", "error_removing_assets_from_album": "Medioiden poisto epäonnistui. Katso konsolista lisätietoja",
"error_selecting_all_assets": "Kaikkia medioita ei voitu valita", "error_selecting_all_assets": "Kaikkia medioita ei voitu valita",
"exclusion_pattern_already_exists": "Tämä poissulkemismalli on jo olemassa.", "exclusion_pattern_already_exists": "Tämä poissulkemismalli on jo olemassa.",
@@ -616,8 +573,6 @@
"failed_to_get_people": "Henkilöiden haku epäonnistui", "failed_to_get_people": "Henkilöiden haku epäonnistui",
"failed_to_load_asset": "Kohteen lataus epäonnistui", "failed_to_load_asset": "Kohteen lataus epäonnistui",
"failed_to_load_assets": "Kohteiden lataus epäonnistui", "failed_to_load_assets": "Kohteiden lataus epäonnistui",
"failed_to_load_people": "Henkilöiden lataus epäonnistui",
"failed_to_remove_product_key": "Tuoteavaimen poistaminen epäonnistui",
"failed_to_stack_assets": "Medioiden pinoaminen epäonnistui", "failed_to_stack_assets": "Medioiden pinoaminen epäonnistui",
"failed_to_unstack_assets": "Medioiden pinoamisen purku epäonnistui", "failed_to_unstack_assets": "Medioiden pinoamisen purku epäonnistui",
"import_path_already_exists": "Tämä tuontipolku on jo olemassa.", "import_path_already_exists": "Tämä tuontipolku on jo olemassa.",
@@ -625,90 +580,54 @@
"paths_validation_failed": "{paths, plural, one {# polun} other {# polun}} validointi epäonnistui", "paths_validation_failed": "{paths, plural, one {# polun} other {# polun}} validointi epäonnistui",
"profile_picture_transparent_pixels": "Profiilikuvassa ei voi olla läpinäkyviä pikseleitä. Zoomaa lähemmäs ja/tai siirrä kuvaa.", "profile_picture_transparent_pixels": "Profiilikuvassa ei voi olla läpinäkyviä pikseleitä. Zoomaa lähemmäs ja/tai siirrä kuvaa.",
"quota_higher_than_disk_size": "Asettamasi kiintiö on suurempi kuin levyn koko", "quota_higher_than_disk_size": "Asettamasi kiintiö on suurempi kuin levyn koko",
"repair_unable_to_check_items": "Ei voida tarkistaa {count, select, one {kohdetta} other {kohteita}}", "unable_to_add_album_users": "",
"unable_to_add_album_users": "Käyttäjiä ei voi lisätä albumiin", "unable_to_add_comment": "",
"unable_to_add_assets_to_shared_link": "Medioiden lisääminen jaettuun linkkiin epäonnistui", "unable_to_add_partners": "",
"unable_to_add_comment": "Kommentin lisääminen epäonnistui", "unable_to_change_album_user_role": "",
"unable_to_add_exclusion_pattern": "Ei voida lisätä poissulkuohjetta", "unable_to_change_date": "",
"unable_to_add_import_path": "Tuontipolkua ei voitu lisätä",
"unable_to_add_partners": "Kumppaneita ei voitu lisätä",
"unable_to_add_remove_archive": "Ei voida {archived, select, true {poistaa kohdetta arkistosta} other {lisätä kohdetta arkistoon}}",
"unable_to_add_remove_favorites": "Ei voida {favorite, select, true {lisätä kohdetta suosikkeihin} other {poistaa kohdetta suosikeista}}",
"unable_to_archive_unarchive": "Ei voida {archived, select, true {arkistoida} other {poistaa arkistosta}}",
"unable_to_change_album_user_role": "Albumin käyttäjän roolia ei voitu muuttaa",
"unable_to_change_date": "Päivämäärää ei voitu muuttaa",
"unable_to_change_favorite": "Ei voida muuttaa suosikkia kohteelle",
"unable_to_change_location": "Sijainnin muuttaminen epäonnistui", "unable_to_change_location": "Sijainnin muuttaminen epäonnistui",
"unable_to_change_password": "Salasanan vaihto epäonnistui", "unable_to_change_password": "Salasanan vaihto epäonnistui",
"unable_to_change_visibility": "Ei voida muuttaa näkyvyyttä {count, plural, one {# henkilölle} other {# henkilölle}}",
"unable_to_check_item": "", "unable_to_check_item": "",
"unable_to_check_items": "", "unable_to_check_items": "",
"unable_to_complete_oauth_login": "OAuth-kirjautumista ei voitu suorittaa loppuun",
"unable_to_connect": "Yhteyttä ei voitu muodostaa",
"unable_to_connect_to_server": "Palvelimeen ei saatu yhteyttä", "unable_to_connect_to_server": "Palvelimeen ei saatu yhteyttä",
"unable_to_copy_to_clipboard": "Leikepöydälle ei voitu kopioida, varmista että käytät sivua https-yhteyden kautta",
"unable_to_create_admin_account": "Pääkäyttäjän luominen epäonnistui", "unable_to_create_admin_account": "Pääkäyttäjän luominen epäonnistui",
"unable_to_create_api_key": "Uuden API-avaimen luominen epäonnistui", "unable_to_create_library": "",
"unable_to_create_library": "Kirjaston luominen epäonnistui", "unable_to_create_user": "",
"unable_to_create_user": "Käyttäjän luominen epäonnistui", "unable_to_delete_album": "",
"unable_to_delete_album": "Albumin poistaminen epäonnistui", "unable_to_delete_asset": "",
"unable_to_delete_asset": "Kohteen poistaminen epäonnistui", "unable_to_delete_user": "",
"unable_to_delete_assets": "Virhe kohteen poistamisessa", "unable_to_empty_trash": "",
"unable_to_delete_exclusion_pattern": "Ei voida poistaa poissulkuohjetta", "unable_to_enter_fullscreen": "",
"unable_to_delete_import_path": "Tuontipolkua ei voitu poistaa", "unable_to_exit_fullscreen": "",
"unable_to_delete_shared_link": "Jaetun linkin poistaminen epäonnistui", "unable_to_hide_person": "",
"unable_to_delete_user": "Käyttäjän poistaminen epäonnistui", "unable_to_load_album": "",
"unable_to_download_files": "Tiedostojen lataaminen epäonnistui", "unable_to_load_asset_activity": "",
"unable_to_edit_exclusion_pattern": "Ei voida muokata poissulkuohjetta", "unable_to_load_items": "",
"unable_to_edit_import_path": "Tuontipolkua ei voitu muokata", "unable_to_load_liked_status": "",
"unable_to_empty_trash": "Roskakorin tyhjentäminen epäonnistui", "unable_to_play_video": "",
"unable_to_enter_fullscreen": "Koko ruudun tilaan siirtyminen epäonnistui", "unable_to_refresh_user": "",
"unable_to_exit_fullscreen": "Koko ruudun tilasta poistuminen epäonnistui", "unable_to_remove_album_users": "",
"unable_to_get_comments_number": "Kommenttien määrän hakeminen epäonnistui",
"unable_to_get_shared_link": "Jaetun linkin hakeminen epäonnistui",
"unable_to_hide_person": "Henkilön piilottaminen epäonnistui",
"unable_to_link_motion_video": "Liikekuvan linkitys epäonnistui",
"unable_to_link_oauth_account": "OAuth-tilin linkittäminen epäonnistui",
"unable_to_load_album": "Albumin lataaminen epäonnistui",
"unable_to_load_asset_activity": "Ei voitu ladata kohteen toimintaa",
"unable_to_load_items": "Kohteiden lataaminen epäonnistui",
"unable_to_load_liked_status": "Ei voitu ladata tykkäyksen tilaa",
"unable_to_log_out_all_devices": "Kaikkien laitteiden uloskirjautuminen epäonnistui",
"unable_to_log_out_device": "Laitteen uloskirjautuminen epäonnistui",
"unable_to_login_with_oauth": "OAuth-kirjautuminen epäonnistui",
"unable_to_play_video": "Videon toistaminen epäonnistui",
"unable_to_reassign_assets_existing_person": "Ei voida siirtää kohteita {name, select, null {olemassa olevalle henkilölle} other {{name}}}",
"unable_to_reassign_assets_new_person": "Ei voida siirtää kohteita uudelle henkilölle",
"unable_to_refresh_user": "Käyttäjän päivittäminen epäonnistui",
"unable_to_remove_album_users": "Käyttäjien poistaminen albumista epäonnistui",
"unable_to_remove_api_key": "API-avaimen poistaminen epäonnistui",
"unable_to_remove_assets_from_shared_link": "kohteiden poistaminen jaetusta linkistä epäonnistui",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_library": "Kirjaston poistaminen epäonnistui", "unable_to_remove_library": "",
"unable_to_remove_offline_files": "Offline-tiedostojen poistaminen epäonnistui", "unable_to_remove_partner": "",
"unable_to_remove_partner": "Kumppanin poistaminen epäonnistui", "unable_to_remove_reaction": "",
"unable_to_remove_reaction": "Reaktion poistaminen epäonnistui",
"unable_to_remove_user": "", "unable_to_remove_user": "",
"unable_to_repair_items": "Kohteiden korjaaminen epäonnistui", "unable_to_repair_items": "",
"unable_to_reset_password": "Salasanan nollaaminen epäonnistui", "unable_to_reset_password": "",
"unable_to_resolve_duplicate": "Virheilmoitus näkyy, kun palvelin palauttaa virheen painettaessa roskakorin tai säilytä-painiketta.", "unable_to_resolve_duplicate": "",
"unable_to_restore_assets": "Kohteen palauttaminen epäonnistui", "unable_to_restore_assets": "",
"unable_to_restore_trash": "Kohteiden palauttaminen epäonnistui", "unable_to_restore_trash": "",
"unable_to_restore_user": "Käyttäjän palauttaminen epäonnistui", "unable_to_restore_user": "",
"unable_to_save_album": "Albumin tallentaminen epäonnistui", "unable_to_save_album": "",
"unable_to_save_api_key": "API-avaimen tallentaminen epäonnistui", "unable_to_save_name": "",
"unable_to_save_date_of_birth": "Syntymäajan tallentaminen epäonnistui", "unable_to_save_profile": "",
"unable_to_save_name": "Nimen tallentaminen epäonnistui", "unable_to_save_settings": "",
"unable_to_save_profile": "Profiilin tallentaminen epäonnistui", "unable_to_scan_libraries": "",
"unable_to_save_settings": "Asetusten tallentaminen epäonnistui", "unable_to_scan_library": "",
"unable_to_scan_libraries": "Kirjastojen skannaaminen epäonnistui",
"unable_to_scan_library": "Kirjaston skannaaminen epäonnistui",
"unable_to_set_feature_photo": "Ei voida asettaa ominaiskuvaa",
"unable_to_set_profile_picture": "Profiilikuvan asetus epäonnistui", "unable_to_set_profile_picture": "Profiilikuvan asetus epäonnistui",
"unable_to_submit_job": "Työtä ei voitu lähettää", "unable_to_submit_job": "Työtä ei voitu lähettää",
"unable_to_trash_asset": "Median siirto roskakoriin epäonnistui", "unable_to_trash_asset": "Median siirto roskakoriin epäonnistui",
"unable_to_unlink_account": "Tunnuksen irroitus epäonnistui", "unable_to_unlink_account": "Tunnuksen irroitus epäonnistui",
"unable_to_unlink_motion_video": "Ei voida irrottaa liikevideota",
"unable_to_update_album_cover": "Albumin kannen päivitys epäonnistui", "unable_to_update_album_cover": "Albumin kannen päivitys epäonnistui",
"unable_to_update_album_info": "Albumin tietojen päivitys epäonnistui", "unable_to_update_album_info": "Albumin tietojen päivitys epäonnistui",
"unable_to_update_library": "Kirjaston päivitys epäonnistui", "unable_to_update_library": "Kirjaston päivitys epäonnistui",
@@ -729,82 +648,59 @@
"expired": "Voimassaolo päättynyt", "expired": "Voimassaolo päättynyt",
"expires_date": "Vanhenee {date}", "expires_date": "Vanhenee {date}",
"explore": "Tutki", "explore": "Tutki",
"explorer": "Tutkija",
"export": "Vie", "export": "Vie",
"export_as_json": "Vie JSON-muodossa", "export_as_json": "Vie JSON-muodossa",
"extension": "Tiedostopääte", "extension": "",
"external": "Ulkoisesta", "external_libraries": "",
"external_libraries": "Ulkoiset kirjastot",
"face_unassigned": "Ei määritelty",
"failed_to_get_people": "", "failed_to_get_people": "",
"favorite": "Suosikki", "favorite": "Suosikki",
"favorite_or_unfavorite_photo": "Suosikki- tai ei-suosikkikuva", "favorite_or_unfavorite_photo": "",
"favorites": "Suosikit", "favorites": "Suosikit",
"feature": "", "feature": "",
"feature_photo_updated": "Kansikuva ladattu", "feature_photo_updated": "Kansikuva ladattu",
"featurecollection": "", "featurecollection": "",
"features": "Ominaisuudet", "file_name": "",
"features_setting_description": "Hallitse sovelluksen ominaisuuksia", "file_name_or_extension": "",
"file_name": "Tiedoston nimi",
"file_name_or_extension": "Tiedostonimi tai tiedostopääte",
"filename": "Tiedostonimi", "filename": "Tiedostonimi",
"files": "", "files": "",
"filetype": "Tiedostotyyppi", "filetype": "Tiedostotyyppi",
"filter_people": "Suodata henkilöt", "filter_people": "",
"find_them_fast": "Löydä nopeasti hakemalla nimellä", "fix_incorrect_match": "",
"fix_incorrect_match": "Korjaa virheellinen osuma", "force_re-scan_library_files": "",
"folders": "Kansiot",
"folders_feature_description": "Käytetään kansionäkymää valokuvien ja videoiden selaamiseen järjestelmässä",
"force_re-scan_library_files": "Pakota kaikkien kirjastotiedostojen uudelleenskannaus",
"forward": "Eteenpäin", "forward": "Eteenpäin",
"general": "Yleinen", "general": "",
"get_help": "Hae apua", "get_help": "",
"getting_started": "Aloittaminen", "getting_started": "",
"go_back": "Palaa", "go_back": "Palaa",
"go_to_search": "Siirry hakuun", "go_to_search": "",
"go_to_share_page": "", "go_to_share_page": "",
"group_albums_by": "Ryhmitä albumi...", "group_albums_by": "",
"group_no": "Ei ryhmitystä", "group_no": "Ei ryhmitystä",
"group_owner": "Ryhmitä omistajan mukaan", "group_owner": "Ryhmitä omistajan mukaan",
"group_year": "Ryhmitä vuoden mukaan", "group_year": "Ryhmitä vuoden mukaan",
"has_quota": "On kiintiö", "has_quota": "",
"hi_user": "Hei {name} ({email})", "hi_user": "Hei {name} ({email})",
"hide_all_people": "Piilota kaikki henkilöt", "hide_gallery": "",
"hide_gallery": "Piilota galleria", "hide_password": "",
"hide_named_person": "Piilota henkilön {name}", "hide_person": "",
"hide_password": "Piilota salasana", "host": "",
"hide_person": "Piilota henkilö",
"hide_unnamed_people": "Piilota nimeämättömät henkilöt",
"host": "Isäntä",
"hour": "Tunti", "hour": "Tunti",
"image": "Kuva", "image": "Kuva",
"image_alt_text_date": "{isVideo, select, true {Video} other {Kuva}} otettu {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Kuva}} otettu {person1} kanssa {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n ja {person2}n kanssa {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n, {person2}n ja {person3}n kanssa {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n, {person2}n ja {additionalCount, number} muissa kanssa {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n kanssa {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n ja {person2}n kanssa {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n, {person2}n ja {person3}n kanssa {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n, {person2}n ja {additionalCount, number} muun kanssa {date}",
"img": "", "img": "",
"immich_logo": "Immich Logo", "immich_logo": "",
"immich_web_interface": "Immich verkkoliittymä", "import_path": "",
"import_from_json": "Tuo JSON-tiedostosta",
"import_path": "Tuontipolku",
"in_albums": "{count, plural, one {# Albumissa} other {# albumissa}}", "in_albums": "{count, plural, one {# Albumissa} other {# albumissa}}",
"in_archive": "Arkistossa", "in_archive": "Arkistossa",
"include_archived": "Sisällytä arkistoidut", "include_archived": "Sisällytä arkistoidut",
"include_shared_albums": "Sisällytä jaetut albumit", "include_shared_albums": "",
"include_shared_partner_assets": "Sisällytä jaetut kumppanikohteet", "include_shared_partner_assets": "",
"individual_share": "Yksittäinen jako", "individual_share": "",
"info": "Lisätietoja", "info": "Lisätietoja",
"interval": { "interval": {
"day_at_onepm": "Joka päivä klo 13:00", "day_at_onepm": "",
"hours": "Joka {hours, plural, one {tunti} other {{hours, number} tuntia}}", "hours": "",
"night_at_midnight": "Joka yö keskiyöllä", "night_at_midnight": "",
"night_at_twoam": "Joka yö klo 02:00" "night_at_twoam": ""
}, },
"invite_people": "Kutsu ihmisiä", "invite_people": "Kutsu ihmisiä",
"invite_to_album": "Kutsu albumiin", "invite_to_album": "Kutsu albumiin",
@@ -818,58 +714,47 @@
"language_setting_description": "Valitse suosimasi kieli", "language_setting_description": "Valitse suosimasi kieli",
"last_seen": "Viimeksi nähty", "last_seen": "Viimeksi nähty",
"latest_version": "Viimeisin versio", "latest_version": "Viimeisin versio",
"latitude": "Leveysaste",
"leave": "Lähde", "leave": "Lähde",
"let_others_respond": "Anna muiden vastata", "let_others_respond": "Anna muiden vastata",
"level": "Taso", "level": "Taso",
"library": "Kirjasto", "library": "Kirjasto",
"library_options": "Kirjastovaihtoehdot", "library_options": "",
"license_button_buy": "Osta", "license_button_buy": "Osta",
"license_button_select": "Valitse", "license_button_select": "Valitse",
"light": "Vaalea", "light": "Vaalea",
"like_deleted": "Tykkäys poistettu", "link_options": "",
"link_motion_video": "Linkitä liikevideo", "link_to_oauth": "",
"link_options": "Linkin asetukset", "linked_oauth_account": "",
"link_to_oauth": "Linkki OAuth",
"linked_oauth_account": "Linkitetty OAuth-tili",
"list": "Lista", "list": "Lista",
"loading": "Ladataan", "loading": "Ladataan",
"loading_search_results_failed": "Hakutulosten lataaminen epäonnistui", "loading_search_results_failed": "",
"log_out": "Kirjaudu ulos", "log_out": "Kirjaudu ulos",
"log_out_all_devices": "Kirjaudu ulos kaikilta laitteilta", "log_out_all_devices": "Kirjaudu ulos kaikilta laitteilta",
"logged_out_all_devices": "Kaikki laitteet kirjattu ulos",
"logged_out_device": "Laite kirjattu ulos",
"login": "Kirjaudu", "login": "Kirjaudu",
"login_has_been_disabled": "Kirjautuminen on otettu pois käytöstä.", "login_has_been_disabled": "Kirjautuminen on otettu pois käytöstä.",
"logout_all_device_confirmation": "Haluatko varmasti kirjautua ulos kaikilta laitteilta?", "logout_all_device_confirmation": "Haluatko varmasti kirjautua ulos kaikilta laitteilta?",
"logout_this_device_confirmation": "Haluatko varmasti kirjautua ulos näiltä laitteilta?", "logout_this_device_confirmation": "Haluatko varmasti kirjautua ulos näiltä laitteilta?",
"longitude": "Pituusaste",
"look": "Tyyli", "look": "Tyyli",
"loop_videos": "Toista videot uudelleen", "loop_videos": "",
"loop_videos_description": "Ota käyttöön videon automaattinen toisto tarkemmassa näkymässä.", "loop_videos_description": "",
"make": "Valmistaja", "make": "Valmistaja",
"manage_shared_links": "Hallitse jaettuja linkkejä", "manage_shared_links": "Hallitse jaettuja linkkejä",
"manage_sharing_with_partners": "Hallitse jakamista kumppaneille", "manage_sharing_with_partners": "",
"manage_the_app_settings": "Hallitse sovelluksen asetuksia", "manage_the_app_settings": "Hallitse sovelluksen asetuksia",
"manage_your_account": "Hallitse tiliäsi", "manage_your_account": "Hallitse tiliäsi",
"manage_your_api_keys": "Hallitse API-avaimiasi", "manage_your_api_keys": "",
"manage_your_devices": "Hallitse sisäänkirjautuneita laitteitasi", "manage_your_devices": "",
"manage_your_oauth_connection": "Hallitse OAuth-yhteyttäsi", "manage_your_oauth_connection": "",
"map": "Kartta", "map": "Kartta",
"map_marker_for_images": "Karttamarkerointi kuville, jotka on otettu {city}ssä, {country}ssä", "map_marker_with_image": "",
"map_marker_with_image": "Karttamarkerointi kuvalla",
"map_settings": "Kartta-asetukset", "map_settings": "Kartta-asetukset",
"matches": "Osumia",
"media_type": "Median tyyppi", "media_type": "Median tyyppi",
"memories": "Muistoja", "memories": "",
"memories_setting_description": "Hallitse mitä näet muistoissasi", "memories_setting_description": "",
"memory": "Muisto", "memory": "Muisto",
"memory_lane_title": "Muistojen polku {title}",
"menu": "Valikko", "menu": "Valikko",
"merge": "Yhdistä", "merge": "Yhdistä",
"merge_people": "Yhdistä henkilöt", "merge_people": "Yhdistä henkilöt",
"merge_people_limit": "Voit yhdistää vain enintään 5 kasvoa kerrallaan",
"merge_people_prompt": "Haluatko yhdistää nämä henkilöt? Tätä valintaa ei voi peruuttaa.",
"merge_people_successfully": "Henkilöt yhdistetty", "merge_people_successfully": "Henkilöt yhdistetty",
"merged_people_count": "{count, plural, one {# Henkilö} other {# henkilöä}} yhdistetty", "merged_people_count": "{count, plural, one {# Henkilö} other {# henkilöä}} yhdistetty",
"minimize": "PIenennä", "minimize": "PIenennä",
@@ -883,7 +768,6 @@
"name": "Nimi", "name": "Nimi",
"name_or_nickname": "Nimi tai lempinimi", "name_or_nickname": "Nimi tai lempinimi",
"never": "ei koskaan", "never": "ei koskaan",
"new_album": "Uusi Albumi",
"new_api_key": "Uusi API Key", "new_api_key": "Uusi API Key",
"new_password": "Uusi salasana", "new_password": "Uusi salasana",
"new_person": "Uusi henkilö", "new_person": "Uusi henkilö",
@@ -896,55 +780,42 @@
"no_albums_message": "Luo albumi pitääksesi kuvat ja videot järjestyksessä", "no_albums_message": "Luo albumi pitääksesi kuvat ja videot järjestyksessä",
"no_albums_with_name_yet": "Näyttää siltä, ettei sinulla ole yhtään tämän nimistä albumia.", "no_albums_with_name_yet": "Näyttää siltä, ettei sinulla ole yhtään tämän nimistä albumia.",
"no_albums_yet": "Näyttää siltä, ettei sinulla ole vielä yhtään albumia.", "no_albums_yet": "Näyttää siltä, ettei sinulla ole vielä yhtään albumia.",
"no_archived_assets_message": "Arkistoi kuvia ja videoita piilottaaksesi ne kuvat näkymästä", "no_archived_assets_message": "",
"no_assets_message": "NAPAUTA LATAAKSESI ENSIMMÄISEN KUVASI", "no_assets_message": "NAPAUTA LATAAKSESI ENSIMMÄISEN KUVASI",
"no_duplicates_found": "Kaksoiskappaleita ei löytynyt.",
"no_exif_info_available": "EXIF-tietoa ei saatavilla", "no_exif_info_available": "EXIF-tietoa ei saatavilla",
"no_explore_results_message": "Lataa lisää kuvia tutkiaksesi kokoelmaasi.", "no_explore_results_message": "",
"no_favorites_message": "Lisää suosikkeja löytääksesi nopeasti parhaat kuvasi ja videosi", "no_favorites_message": "Lisää suosikkeja löytääksesi nopeasti parhaat kuvasi ja videosi",
"no_libraries_message": "Luo ulkoinen kirjasto nähdäksesi valokuvasi ja videot", "no_libraries_message": "",
"no_name": "Ei nimeä", "no_name": "Ei nimeä",
"no_places": "Ei paikkoja", "no_places": "",
"no_results": "Ei tuloksia", "no_results": "Ei tuloksia",
"no_results_description": "Kokeile synonyymiä tai yleisempää avainsanaa",
"no_shared_albums_message": "Luo albumi, jotta voit jakaa kuvia ja videoita toisille", "no_shared_albums_message": "Luo albumi, jotta voit jakaa kuvia ja videoita toisille",
"not_in_any_album": "Ei yhdessäkään albumissa", "not_in_any_album": "Ei yhdessäkään albumissa",
"note_apply_storage_label_to_previously_uploaded assets": "Huom: Jotta voit soveltaa tallennustunnistetta aiemmin ladattuihin kohteisiin, suorita",
"note_unlimited_quota": "Huomio: Syötä 0 rajoittamatonta kiintiötä varten",
"notes": "Muistiinpanot", "notes": "Muistiinpanot",
"notification_toggle_setting_description": "Ota sähköpostilmoitukset käyttöön", "notification_toggle_setting_description": "Ota sähköpostilmoitukset käyttöön",
"notifications": "Ilmoitukset", "notifications": "Ilmoitukset",
"notifications_setting_description": "Hallitse ilmoituksia", "notifications_setting_description": "Hallitse ilmoituksia",
"oauth": "OAuth", "oauth": "OAuth",
"offline": "Offline", "offline": "",
"offline_paths": "Offline-polut",
"offline_paths_description": "Nämä tulokset voivat johtua tiedostojen manuaalisesta poistamisesta, jotka eivät ole osa ulkoista kirjastoa.",
"ok": "Ok", "ok": "Ok",
"oldest_first": "Vanhin ensin", "oldest_first": "Vanhin ensin",
"onboarding": "Käyttöönotto",
"onboarding_privacy_description": "Seuraavat (valinnaiset) ominaisuudet perustuvat ulkoisiin palveluihin, ja ne voidaan poistaa käytöstä milloin tahansa hallinta asetuksista.",
"onboarding_theme_description": "Valitse väriteema istunnollesi. Voit muuttaa tämän myöhemmin asetuksistasi.",
"onboarding_welcome_description": "Aloitetaa laittamalla istuntoosi joitakin yleisiä asetuksia.",
"onboarding_welcome_user": "Tervetuloa {user}", "onboarding_welcome_user": "Tervetuloa {user}",
"online": "Online", "online": "Online",
"only_favorites": "Vain suosikit", "only_favorites": "Vain suosikit",
"only_refreshes_modified_files": "Päivittää vain muakatut tiedostot", "only_refreshes_modified_files": "",
"open_in_map_view": "Avaa karttanäkymässä",
"open_in_openstreetmap": "Avaa OpenStreetMapissa", "open_in_openstreetmap": "Avaa OpenStreetMapissa",
"open_the_search_filters": "Avaa hakusuodattimet", "open_the_search_filters": "",
"options": "Vaihtoehdot", "options": "Vaihtoehdot",
"or": "tai", "or": "tai",
"organize_your_library": "Järjestele kirjastosi", "organize_your_library": "Järjestele kirjastosi",
"original": "alkuperäinen", "original": "alkuperäinen",
"other": "Muut", "other": "Muut",
"other_devices": "Toiset laitteet", "other_devices": "Toiset laitteet",
"other_variables": "Muut muuttujat", "other_variables": "",
"owned": "Omistettu", "owned": "Omistettu",
"owner": "Omistaja", "owner": "Omistaja",
"partner": "Kumppani", "partner": "Kumppani",
"partner_can_access": "{partner} voi päästä", "partner_can_access": "{partner} voi päästä",
"partner_can_access_assets": "Kaikki valokuvasi ja videosi, lukuun ottamatta arkistoituja ja poistettuja",
"partner_can_access_location": "Sijainti, jossa kuvasi on otettu",
"partner_sharing": "Kumppanijako", "partner_sharing": "Kumppanijako",
"partners": "Kumppanit", "partners": "Kumppanit",
"password": "Salasana", "password": "Salasana",
@@ -952,26 +823,22 @@
"password_required": "Salasana vaaditaan", "password_required": "Salasana vaaditaan",
"password_reset_success": "Salasanan nollaus onnistui", "password_reset_success": "Salasanan nollaus onnistui",
"past_durations": { "past_durations": {
"days": "Viime {days, plural, one {päivä} other {# päivää}}", "days": "{years, plural, one {Viimeisin päivä} other {Viimeiset # päivää}}",
"hours": "Viime {hours, plural, one {tunti} other {# tuntia}}", "hours": "{years, plural, one {Viimeisin tunti} other {Viimeiset # tuntia}}",
"years": "{years, plural, one {Viimeisin vuosi} other {Viimeiset # vuotta}}" "years": "{years, plural, one {Viimeisin vuosi} other {Viimeiset # vuotta}}"
}, },
"path": "Polku", "path": "Polku",
"pattern": "Kaava", "pattern": "",
"pause": "Tauko", "pause": "Tauko",
"pause_memories": "Pysäytä muistot", "pause_memories": "",
"paused": "Tauotettu", "paused": "Tauotettu",
"pending": "Odottaa", "pending": "Odottaa",
"people": "Ihmiset", "people": "Ihmiset",
"people_edits_count": "Muokattu {count, plural, one {# henkilö} other {# henkilöä}}", "people_sidebar_description": "",
"people_feature_description": "Selataan valokuvia ja videoita, jotka on ryhmitelty henkilöiden mukaan",
"people_sidebar_description": "Näytä linkki Henkilöihin sivupalkissa",
"perform_library_tasks": "", "perform_library_tasks": "",
"permanent_deletion_warning": "Pysyvän poiston varoitus", "permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "Näytä varoitus, kun poistat kohteita pysyvästi", "permanent_deletion_warning_setting_description": "",
"permanently_delete": "Poista pysyvästi", "permanently_delete": "Poista pysyvästi",
"permanently_delete_assets_count": "Poista pysyvästi {count, plural, one {kohde} other {kohteita}}",
"permanently_delete_assets_prompt": "Oletko varma, että haluat poistaa pysyvästi {count, plural, one {tämän kohteen?} other {nämä <b>#</b> kohteet?}} Tämä poistaa myös {count, plural, one {sen sen} other {ne niiden}} albumista.",
"permanently_deleted_asset": "Media poistettu pysyvästi", "permanently_deleted_asset": "Media poistettu pysyvästi",
"permanently_deleted_assets_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi", "permanently_deleted_assets_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi",
"person": "Henkilö", "person": "Henkilö",
@@ -986,7 +853,7 @@
"places": "Paikat", "places": "Paikat",
"play": "Toista", "play": "Toista",
"play_memories": "Toista muistot", "play_memories": "Toista muistot",
"play_motion_photo": "Toista Liikekuva", "play_motion_photo": "",
"play_or_pause_video": "Toista tai keskeytä video", "play_or_pause_video": "Toista tai keskeytä video",
"point": "", "point": "",
"port": "Portti", "port": "Portti",
@@ -996,53 +863,15 @@
"previous_memory": "Edellinen muisto", "previous_memory": "Edellinen muisto",
"previous_or_next_photo": "Edellinen tai seuraava kuva", "previous_or_next_photo": "Edellinen tai seuraava kuva",
"primary": "Ensisijainen", "primary": "Ensisijainen",
"privacy": "Yksityisyys",
"profile_image_of_user": "Käyttäjän {user} profiilikuva", "profile_image_of_user": "Käyttäjän {user} profiilikuva",
"profile_picture_set": "Profiilikuva asetettu.", "profile_picture_set": "Profiilikuva asetettu.",
"public_album": "Julkinen albumi", "public_album": "Julkinen albumi",
"public_share": "Julkinen jako", "public_share": "Julkinen jako",
"purchase_account_info": "Tukija",
"purchase_activated_subtitle": "Kiitos Immichin ja avoimen lähdekoodin ohjelmiston tukemisesta",
"purchase_activated_time": "Aktivoitu {date, date}",
"purchase_activated_title": "Avaimesi on aktivoitu onnistuneesti",
"purchase_button_activate": "Aktivoi",
"purchase_button_buy": "Osta",
"purchase_button_buy_immich": "Osta Immich",
"purchase_button_never_show_again": "Älä näytä koskaan uudelleen",
"purchase_button_reminder": "Muistuta minua 30 päivän kuluessa",
"purchase_button_remove_key": "Poista avain",
"purchase_button_select": "Valitse",
"purchase_failed_activation": "Aktivointi epäonnistui! Tarkista sähköpostisi oikean tuoteavaimen varalta!",
"purchase_individual_description_1": "Yksittäiselle henkilölle",
"purchase_individual_description_2": "Tukijan tila",
"purchase_individual_title": "Yksittäinen",
"purchase_input_suggestion": "Onko sinulla tuoteavain? Syötä avain alle",
"purchase_license_subtitle": "Osta Immich tukeaksesi palvelun jatkuvaa kehittämistä",
"purchase_lifetime_description": "Elinikäinen osto",
"purchase_option_title": "OSTOVAIHTOEHDOT",
"purchase_panel_info_1": "Immichin rakentaminen vie paljon aikaa ja vaivannäköä, ja meillä on kokopäiväisiä insinöörejä työskentelemässä sen parissa, jotta voimme tehdä siitä mahdollisimman hyvän. Missiomme on, että avoimen lähdekoodin ohjelmistosta ja eettisistä liiketoimintakäytännöistä tulee kestävä tulonlähde kehittäjille, sekä luoda yksityisyyttä kunnioittava ekosysteemi, jossa on todellisia vaihtoehtoja hyväksikäyttöön perustuville pilvipalveluille.",
"purchase_panel_info_2": "Koska olemme sitoutuneet siihen, ettemme lisää maksumuuria, tämä osto ei anna sinulle mitään lisäominaisuuksia Immichissa. Luotamme kaltaisiisi käyttäjiin tukeaksemme Immichin jatkuvaa kehittämistä.",
"purchase_panel_title": "Tue projektia",
"purchase_per_server": "Per serveri",
"purchase_per_user": "Per käyttäjä",
"purchase_remove_product_key": "Poista Tuoteavain",
"purchase_remove_product_key_prompt": "Haluatko varmasti poistaa tuoteavaimen?",
"purchase_remove_server_product_key": "Poista palvelimen tuoteavain",
"purchase_remove_server_product_key_prompt": "Haluatko varmasti poistaa palvelimen tuoteavaimen?",
"purchase_server_description_1": "Koko palvelimelle",
"purchase_server_description_2": "Tukijan tila",
"purchase_server_title": "Serveri",
"purchase_settings_server_activated": "Palvelimen tuoteavainta hallinnoi ylläpitäjä",
"range": "", "range": "",
"rating": "Tähtiarvostelu",
"rating_clear": "Tyhjennä arvostelu",
"rating_count": "{count, plural, one {# tähti} other {# tähteä}}",
"rating_description": "Näytä EXIF-arvosana tiedot-paneelissa",
"raw": "", "raw": "",
"reaction_options": "Reaktioasetukset", "reaction_options": "",
"read_changelog": "Lue muutosloki", "read_changelog": "Lue muutosloki",
"reassign": "Määritä uudelleen", "reassign": "Määritä uudelleen",
"reassigned_assets_to_existing_person": "Uudelleen määritetty {count, plural, one {# kohde} other {# kohdetta}} {name, select, null {olemassa olevalle henkilölle} other {{name}}}",
"reassigned_assets_to_new_person": "Määritetty {count, plural, one {# media} other {# mediaa}} uudelle henkilölle", "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", "reassing_hint": "Määritä valitut mediat käyttäjälle",
"recent": "Viimeisin", "recent": "Viimeisin",
@@ -1061,19 +890,18 @@
"remove_assets_shared_link_confirmation": "Haluatko varmasti poistaa {count, plural, one {# median} other {# mediaa}} tästä jakolinkistä?", "remove_assets_shared_link_confirmation": "Haluatko varmasti poistaa {count, plural, one {# median} other {# mediaa}} tästä jakolinkistä?",
"remove_assets_title": "Poistetaanko?", "remove_assets_title": "Poistetaanko?",
"remove_custom_date_range": "Poista aikaväliltä", "remove_custom_date_range": "Poista aikaväliltä",
"remove_deleted_assets": "Poista Offline-tiedostot",
"remove_from_album": "Poista albumista", "remove_from_album": "Poista albumista",
"remove_from_favorites": "Poista suosikeista", "remove_from_favorites": "Poista suosikeista",
"remove_from_shared_link": "Poista jakolinkistä", "remove_from_shared_link": "Poista jakolinkistä",
"remove_offline_files": "Poista Offline-tiedostot",
"remove_user": "Poista käyttäjä", "remove_user": "Poista käyttäjä",
"removed_api_key": "API Key {name} poistettu", "removed_api_key": "API Key {name} poistettu",
"removed_from_archive": "Poistettu arkistosta", "removed_from_archive": "Poistettu arkistosta",
"removed_from_favorites": "Poistettu suosikeista", "removed_from_favorites": "Poistettu suosikeista",
"removed_from_favorites_count": "{count, plural, other {Poistettu #}} suosikeista", "removed_from_favorites_count": "{count, plural, other {Poistettu #}} suosikeista",
"removed_tagged_assets": "Poistettu tunniste {count, plural, one {# kohteesta} other {# kohteesta}}",
"rename": "Nimeä uudelleen", "rename": "Nimeä uudelleen",
"repair": "Korjaa", "repair": "Korjaa",
"repair_no_results_message": "Seuraamattomat ja puuttuvat tiedostot näkyvät täällä", "repair_no_results_message": "",
"replace_with_upload": "Korvaa tiedostolla", "replace_with_upload": "Korvaa tiedostolla",
"repository": "Tietovarasto", "repository": "Tietovarasto",
"require_password": "Vaadi salasana", "require_password": "Vaadi salasana",
@@ -1083,7 +911,6 @@
"reset_people_visibility": "Nollaa henkilöiden näkyvyysasetukset", "reset_people_visibility": "Nollaa henkilöiden näkyvyysasetukset",
"reset_settings_to_default": "", "reset_settings_to_default": "",
"reset_to_default": "Palauta oletusasetukset", "reset_to_default": "Palauta oletusasetukset",
"resolve_duplicates": "Ratkaise kaksoiskappaleet",
"resolved_all_duplicates": "Kaikki kaksoiskappaleet selvitetty", "resolved_all_duplicates": "Kaikki kaksoiskappaleet selvitetty",
"restore": "Palauta", "restore": "Palauta",
"restore_all": "Palauta kaikki", "restore_all": "Palauta kaikki",
@@ -1093,7 +920,7 @@
"retry_upload": "Yritä latausta uudelleen", "retry_upload": "Yritä latausta uudelleen",
"review_duplicates": "Tarkastele kaksoiskappaleita", "review_duplicates": "Tarkastele kaksoiskappaleita",
"role": "Rooli", "role": "Rooli",
"role_editor": "Editori", "role_editor": "Muokkain",
"role_viewer": "Toistin", "role_viewer": "Toistin",
"save": "Tallenna", "save": "Tallenna",
"saved_api_key": "API Key tallennettu", "saved_api_key": "API Key tallennettu",
@@ -1108,8 +935,6 @@
"search": "Haku", "search": "Haku",
"search_albums": "Etsi albumeita", "search_albums": "Etsi albumeita",
"search_by_context": "Etsi kontekstin perusteella", "search_by_context": "Etsi kontekstin perusteella",
"search_by_filename": "Hae tiedostonimen tai -päätteen mukaan",
"search_by_filename_example": "esim. IMG_1234.JPG tai PNG",
"search_camera_make": "Etsi kameramerkkiä...", "search_camera_make": "Etsi kameramerkkiä...",
"search_camera_model": "Etsi kameramallia...", "search_camera_model": "Etsi kameramallia...",
"search_city": "Etsi kaupunkia...", "search_city": "Etsi kaupunkia...",
@@ -1117,12 +942,9 @@
"search_for_existing_person": "Etsi olemassa olevaa henkilöä", "search_for_existing_person": "Etsi olemassa olevaa henkilöä",
"search_no_people": "Ei henkilöitä", "search_no_people": "Ei henkilöitä",
"search_no_people_named": "Ei \"{name}\" nimisiä henkilöitä", "search_no_people_named": "Ei \"{name}\" nimisiä henkilöitä",
"search_options": "Hakuvaihtoehdot",
"search_people": "Etsi ihmisiä", "search_people": "Etsi ihmisiä",
"search_places": "Etsi paikkoja", "search_places": "Etsi paikkoja",
"search_settings": "Hakuasetukset",
"search_state": "Etsi tilaa...", "search_state": "Etsi tilaa...",
"search_tags": "Haku tageja...",
"search_timezone": "Etsi aikavyöhyke...", "search_timezone": "Etsi aikavyöhyke...",
"search_type": "Etsinnän tyyppi", "search_type": "Etsinnän tyyppi",
"search_your_photos": "Etsi kuvia", "search_your_photos": "Etsi kuvia",
@@ -1131,7 +953,6 @@
"see_all_people": "Näytä kaikki henkilöt", "see_all_people": "Näytä kaikki henkilöt",
"select_album_cover": "Valitse albmin kansi", "select_album_cover": "Valitse albmin kansi",
"select_all": "Valitse kaikki", "select_all": "Valitse kaikki",
"select_all_duplicates": "Valitse kaikki kaksoiskappaleet",
"select_avatar_color": "Valitse avatarin väri", "select_avatar_color": "Valitse avatarin väri",
"select_face": "Valitse kasvo", "select_face": "Valitse kasvo",
"select_featured_photo": "Valitse esittelykuva", "select_featured_photo": "Valitse esittelykuva",
@@ -1146,7 +967,6 @@
"send_message": "Lähetä viesti", "send_message": "Lähetä viesti",
"send_welcome_email": "Lähetä tervetuloviesti", "send_welcome_email": "Lähetä tervetuloviesti",
"server": "Palvelin", "server": "Palvelin",
"server_offline": "Serveri Offline-tilassa",
"server_online": "Palvelin on linjalla", "server_online": "Palvelin on linjalla",
"server_stats": "Palvelimen tilastot", "server_stats": "Palvelimen tilastot",
"server_version": "Palvelimen versio", "server_version": "Palvelimen versio",
@@ -1164,7 +984,6 @@
"shared_by_user": "Käyttäjän {user} jakama", "shared_by_user": "Käyttäjän {user} jakama",
"shared_by_you": "Sinun jakamasi", "shared_by_you": "Sinun jakamasi",
"shared_from_partner": "{partner}n kuvia", "shared_from_partner": "{partner}n kuvia",
"shared_link_options": "Jaetun linkin vaihtoehdot",
"shared_links": "Jaetut linkit", "shared_links": "Jaetut linkit",
"shared_photos_and_videos_count": "{assetCount, plural, other {# jaettua kuvaa ja videota.}}", "shared_photos_and_videos_count": "{assetCount, plural, other {# jaettua kuvaa ja videota.}}",
"shared_with_partner": "Jaa {partner} kanssa", "shared_with_partner": "Jaa {partner} kanssa",
@@ -1173,7 +992,6 @@
"sharing_sidebar_description": "Näytä jakamislinkki sivupalkissa", "sharing_sidebar_description": "Näytä jakamislinkki sivupalkissa",
"shift_to_permanent_delete": "Paina ⇧ poistaaksesi median pysyvästi", "shift_to_permanent_delete": "Paina ⇧ poistaaksesi median pysyvästi",
"show_album_options": "Näytä albumin asetukset", "show_album_options": "Näytä albumin asetukset",
"show_albums": "Näytä albumit",
"show_all_people": "Näytä kaikki henkilöt", "show_all_people": "Näytä kaikki henkilöt",
"show_and_hide_people": "Näytä / piilota henkilöitä", "show_and_hide_people": "Näytä / piilota henkilöitä",
"show_file_location": "Näytä tiedostosijainti", "show_file_location": "Näytä tiedostosijainti",
@@ -1188,17 +1006,11 @@
"show_person_options": "Näytä henkilöasetukset", "show_person_options": "Näytä henkilöasetukset",
"show_progress_bar": "Näytä eteneminen", "show_progress_bar": "Näytä eteneminen",
"show_search_options": "Näytä hakuvaihtoehdot", "show_search_options": "Näytä hakuvaihtoehdot",
"show_supporter_badge": "Kannattajan merkki",
"show_supporter_badge_description": "Näytä kannattajan merkki",
"shuffle": "Sekoita", "shuffle": "Sekoita",
"sidebar": "Sivupalkki",
"sidebar_display_description": "Näytä linkki näkymään sivupalkissa",
"sign_out": "Kirjaudu ulos", "sign_out": "Kirjaudu ulos",
"sign_up": "Rekisteröidy", "sign_up": "Rekisteröidy",
"size": "Koko", "size": "Koko",
"skip_to_content": "Siirry sisältöön", "skip_to_content": "Siirry sisältöön",
"skip_to_folders": "Siirry kansioihin",
"skip_to_tags": "Siirry tageihin",
"slideshow": "Diaesitys", "slideshow": "Diaesitys",
"slideshow_settings": "Diaesityksen asetukset", "slideshow_settings": "Diaesityksen asetukset",
"sort_albums_by": "Järjestä albumit...", "sort_albums_by": "Järjestä albumit...",
@@ -1210,8 +1022,6 @@
"sort_title": "Otsikko", "sort_title": "Otsikko",
"source": "Lähde", "source": "Lähde",
"stack": "Pinoa", "stack": "Pinoa",
"stack_duplicates": "Pinoa kaksoiskappaleet",
"stack_select_one_photo": "Valitse yksi pääkuva pinolle",
"stack_selected_photos": "Pinoa valitut kuvat", "stack_selected_photos": "Pinoa valitut kuvat",
"stacked_assets_count": "Pinottu {count, plural, one {# media} other {# mediaa}}", "stacked_assets_count": "Pinottu {count, plural, one {# media} other {# mediaa}}",
"stacktrace": "Vianetsintätiedot", "stacktrace": "Vianetsintätiedot",
@@ -1231,14 +1041,6 @@
"sunrise_on_the_beach": "Auringonnousu rannalla", "sunrise_on_the_beach": "Auringonnousu rannalla",
"swap_merge_direction": "Käännä yhdistämissuunta", "swap_merge_direction": "Käännä yhdistämissuunta",
"sync": "Synkronoi", "sync": "Synkronoi",
"tag": "Tagi",
"tag_assets": "Merkitse kohde",
"tag_created": "Luotu tunniste: {tag}",
"tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tagiotsikoiden mukaan",
"tag_not_found_question": "Etkö löydä tunnistetta? Luo yksi <link>tästä</link>",
"tag_updated": "Päivitetty tunniste: {tag}",
"tagged_assets": "Tunnistettu {count, plural, one {# kohde} other {# kohdetta}}",
"tags": "Tagit",
"template": "Template", "template": "Template",
"theme": "Teema", "theme": "Teema",
"theme_selection": "Teeman valinta", "theme_selection": "Teeman valinta",
@@ -1250,15 +1052,14 @@
"to_change_password": "Vaihda salasana", "to_change_password": "Vaihda salasana",
"to_favorite": "Aseta suosikiksi", "to_favorite": "Aseta suosikiksi",
"to_login": "Kirjaudu sisään", "to_login": "Kirjaudu sisään",
"to_parent": "Siirry vanhempaan",
"to_trash": "Roskakoriin", "to_trash": "Roskakoriin",
"toggle_settings": "Määritä asetukset", "toggle_settings": "Määritä asetukset",
"toggle_theme": "Aseta tumma teema", "toggle_theme": "Aseta teema",
"toggle_visibility": "Aseta näkyvyys", "toggle_visibility": "Aseta näkyvyys",
"total_usage": "Käyttö yhteensä", "total_usage": "Käyttö yhteensä",
"trash": "Roskakori", "trash": "Roskakori",
"trash_all": "Vie kaikki roskakoriin", "trash_all": "Vie kaikki roskakoriin",
"trash_count": "Roskakori {count, number}", "trash_count": "Vie {count} roskakoriin",
"trash_delete_asset": "Poista / vie roskakoriin", "trash_delete_asset": "Poista / vie roskakoriin",
"trash_no_results_message": "Roskakorissa olevat kuvat ja videot näytetään täällä.", "trash_no_results_message": "Roskakorissa olevat kuvat ja videot näytetään täällä.",
"trashed_items_will_be_permanently_deleted_after": "Roskakorin kohteet poistetaan pysyvästi {days, plural, one {# päivän} other {# päivän}} päästä.", "trashed_items_will_be_permanently_deleted_after": "Roskakorin kohteet poistetaan pysyvästi {days, plural, one {# päivän} other {# päivän}} päästä.",
@@ -1272,17 +1073,13 @@
"unknown_album": "", "unknown_album": "",
"unknown_year": "Tuntematon vuosi", "unknown_year": "Tuntematon vuosi",
"unlimited": "Rajoittamaton", "unlimited": "Rajoittamaton",
"unlink_motion_video": "Poista liikevideon linkitys",
"unlink_oauth": "Poista OAuth-linkitys", "unlink_oauth": "Poista OAuth-linkitys",
"unlinked_oauth_account": "Linkittämätön OAuth-tili", "unlinked_oauth_account": "Linkittämätön OAuth-tili",
"unnamed_album": "Nimetön albumi", "unnamed_album": "Nimetön albumi",
"unnamed_album_delete_confirmation": "Haluatko varmasti poistaa tämän albumin?",
"unnamed_share": "Nimetön jako", "unnamed_share": "Nimetön jako",
"unsaved_change": "Tallentamaton muutos", "unsaved_change": "Tallentamaton muutos",
"unselect_all": "Poista valinnat", "unselect_all": "Poista valinnat",
"unselect_all_duplicates": "Poista kaikkien kaksoiskappaleiden valinta",
"unstack": "Pura pino", "unstack": "Pura pino",
"unstacked_assets_count": "Poistettu pinosta {count, plural, one {# kohde} other {# kohdetta}}",
"untracked_files": "Tiedostot joita ei seurata", "untracked_files": "Tiedostot joita ei seurata",
"untracked_files_decription": "Järjestelmä ei seuraa näitä tiedostoja. Ne voivat johtua epäonnistuneista siirroista, keskeytyneistä latauksista, tai ovat jääneet ohjelmavian seurauksena", "untracked_files_decription": "Järjestelmä ei seuraa näitä tiedostoja. Ne voivat johtua epäonnistuneista siirroista, keskeytyneistä latauksista, tai ovat jääneet ohjelmavian seurauksena",
"up_next": "Seuraavaksi", "up_next": "Seuraavaksi",
@@ -1290,7 +1087,7 @@
"upload": "Siirrä palvelimelle", "upload": "Siirrä palvelimelle",
"upload_concurrency": "Latausten samanaikaisuus", "upload_concurrency": "Latausten samanaikaisuus",
"upload_errors": "Lataus valmistui {count, plural, one {# virheen} other {# virheen}} kanssa. Päivitä sivu nähdäksesi ladatut tiedot.", "upload_errors": "Lataus valmistui {count, plural, one {# virheen} other {# virheen}} kanssa. Päivitä sivu nähdäksesi ladatut tiedot.",
"upload_progress": "Jäljellä {remaining, number} - Käsitelty {processed, number}/{total, number}", "upload_progress": "{remaining} jäljellä - {processed}/{total} käsitelty",
"upload_skipped_duplicates": "Ohitettiin {count, plural, one {# kaksoiskappale} other {# kaksoiskappaletta}}", "upload_skipped_duplicates": "Ohitettiin {count, plural, one {# kaksoiskappale} other {# kaksoiskappaletta}}",
"upload_status_duplicates": "Kaksoiskappaleet", "upload_status_duplicates": "Kaksoiskappaleet",
"upload_status_errors": "Virheet", "upload_status_errors": "Virheet",
@@ -1302,8 +1099,6 @@
"user": "Käyttäjä", "user": "Käyttäjä",
"user_id": "Käyttäjän ID", "user_id": "Käyttäjän ID",
"user_liked": "{user} tykkäsi {type, select, photo {kuvasta} video {videosta} asset {mediasta} other {tästä}}", "user_liked": "{user} tykkäsi {type, select, photo {kuvasta} video {videosta} asset {mediasta} other {tästä}}",
"user_purchase_settings": "Osta",
"user_purchase_settings_description": "Hallitse ostostasi",
"user_role_set": "Tee käyttäjästä {user} {role}", "user_role_set": "Tee käyttäjästä {user} {role}",
"user_usage_detail": "Käyttäjän käytön tiedot", "user_usage_detail": "Käyttäjän käytön tiedot",
"username": "Käyttäjänimi", "username": "Käyttäjänimi",
+15 -22
View File
@@ -41,7 +41,6 @@
"confirm_email_below": "Pour confirmer, tapez « {email} » ci-dessous", "confirm_email_below": "Pour confirmer, tapez « {email} » ci-dessous",
"confirm_reprocess_all_faces": "Êtes-vous sûr de vouloir retraiter tous les visages? Cela effacera également les personnes déjà identifiées.", "confirm_reprocess_all_faces": "Êtes-vous sûr de vouloir retraiter tous les visages? Cela effacera également les personnes déjà identifiées.",
"confirm_user_password_reset": "Êtes-vous sûr de vouloir réinitialiser le mot de passe de {user}?", "confirm_user_password_reset": "Êtes-vous sûr de vouloir réinitialiser le mot de passe de {user}?",
"create_job": "Créer une tâche",
"crontab_guru": "Générateur de règles Cron", "crontab_guru": "Générateur de règles Cron",
"disable_login": "Désactiver la connexion", "disable_login": "Désactiver la connexion",
"disabled": "Désactivé", "disabled": "Désactivé",
@@ -71,7 +70,6 @@
"image_thumbnail_resolution": "Résolution des miniatures", "image_thumbnail_resolution": "Résolution des miniatures",
"image_thumbnail_resolution_description": "Utilisée lors du visionnage de groupes de photos (vue principale, albums, etc.). Une résolution plus élevée préserve davantage de détails, mais est plus longue à encoder, produit des fichiers plus lourds, et peut réduire la réactivité de l'application.", "image_thumbnail_resolution_description": "Utilisée lors du visionnage de groupes de photos (vue principale, albums, etc.). Une résolution plus élevée préserve davantage de détails, mais est plus longue à encoder, produit des fichiers plus lourds, et peut réduire la réactivité de l'application.",
"job_concurrency": "{job}: nombre de tâches simultanées", "job_concurrency": "{job}: nombre de tâches simultanées",
"job_created": "Tâche créée",
"job_not_concurrency_safe": "Cette tâche ne peut pas être exécutée en multitâche de façon sûre.", "job_not_concurrency_safe": "Cette tâche ne peut pas être exécutée en multitâche de façon sûre.",
"job_settings": "Paramètres des tâches", "job_settings": "Paramètres des tâches",
"job_settings_description": "Gestion des tâches simultanées", "job_settings_description": "Gestion des tâches simultanées",
@@ -154,7 +152,7 @@
"note_cannot_be_changed_later": "REMARQUE: Il n'est pas possible de modifier ce paramètre ultérieurement!", "note_cannot_be_changed_later": "REMARQUE: Il n'est pas possible de modifier ce paramètre ultérieurement!",
"note_unlimited_quota": "Note: saisir 0 pour un quota illimité", "note_unlimited_quota": "Note: saisir 0 pour un quota illimité",
"notification_email_from_address": "Depuis l'adresse", "notification_email_from_address": "Depuis l'adresse",
"notification_email_from_address_description": "Adresse courriel de l'expéditeur, par exemple: « Serveur de photos Immich <nepasrepondre@exemple.org> »", "notification_email_from_address_description": "Adresse courriel de l'expéditeur, par exemple: « Serveur de photos Immich <nepasrepondre@immich.app> »",
"notification_email_host_description": "Hôte du serveur de messagerie électronique (par exemple, smtp.immich.app)", "notification_email_host_description": "Hôte du serveur de messagerie électronique (par exemple, smtp.immich.app)",
"notification_email_ignore_certificate_errors": "Ignorer les erreurs de certificat", "notification_email_ignore_certificate_errors": "Ignorer les erreurs de certificat",
"notification_email_ignore_certificate_errors_description": "Ignorer les erreurs de validation du certificat TLS (non recommandé)", "notification_email_ignore_certificate_errors_description": "Ignorer les erreurs de validation du certificat TLS (non recommandé)",
@@ -200,12 +198,11 @@
"password_settings": "Connexion par mot de passe", "password_settings": "Connexion par mot de passe",
"password_settings_description": "Gérer les paramètres de connexion par mot de passe", "password_settings_description": "Gérer les paramètres de connexion par mot de passe",
"paths_validated_successfully": "Tous les chemins ont été validés avec succès", "paths_validated_successfully": "Tous les chemins ont été validés avec succès",
"person_cleanup_job": "Nettoyage des personnes",
"quota_size_gib": "Taille du quota (Go)", "quota_size_gib": "Taille du quota (Go)",
"refreshing_all_libraries": "Actualisation de toutes les bibliothèques", "refreshing_all_libraries": "Actualisation de toutes les bibliothèques",
"registration": "Enregistrement de l'administrateur", "registration": "Enregistrement de l'administrateur",
"registration_description": "Puisque vous êtes le premier utilisateur sur le système, vous serez désigné en tant qu'administrateur et responsable des tâches administratives, et vous pourrez alors créer d'autres utilisateurs.", "registration_description": "Puisque vous êtes le premier utilisateur sur le système, vous serez désigné en tant qu'administrateur et responsable des tâches administratives, et vous pourrez alors créer d'autres utilisateurs.",
"removing_deleted_files": "Suppression des fichiers hors ligne", "removing_offline_files": "Suppression des fichiers hors ligne",
"repair_all": "Réparer tout", "repair_all": "Réparer tout",
"repair_matched_items": "{count, plural, one {# Élément correspondant} other {# Éléments correspondants}}", "repair_matched_items": "{count, plural, one {# Élément correspondant} other {# Éléments correspondants}}",
"repaired_items": "{count, plural, one {# Élément corrigé} other {# Éléments corrigés}}", "repaired_items": "{count, plural, one {# Élément corrigé} other {# Éléments corrigés}}",
@@ -214,7 +211,6 @@
"reset_settings_to_recent_saved": "Paramètres réinitialisés avec les derniers paramètres enregistrés", "reset_settings_to_recent_saved": "Paramètres réinitialisés avec les derniers paramètres enregistrés",
"scanning_library_for_changed_files": "Recherche de fichiers modifiés dans la bibliothèque", "scanning_library_for_changed_files": "Recherche de fichiers modifiés dans la bibliothèque",
"scanning_library_for_new_files": "Recherche de nouveaux fichiers dans la bibliothèque", "scanning_library_for_new_files": "Recherche de nouveaux fichiers dans la bibliothèque",
"search_jobs": "Recherche des tâches ...",
"send_welcome_email": "Envoyer un courriel de bienvenue", "send_welcome_email": "Envoyer un courriel de bienvenue",
"server_external_domain_settings": "Domaine externe", "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 pour les liens partagés publics, y compris http(s)://",
@@ -242,7 +238,6 @@
"storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média envoyé", "storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média envoyé",
"storage_template_user_label": "<code>{label}</code> est l'étiquette de stockage de l'utilisateur", "storage_template_user_label": "<code>{label}</code> est l'étiquette de stockage de l'utilisateur",
"system_settings": "Paramètres du système", "system_settings": "Paramètres du système",
"tag_cleanup_job": "Nettoyage des étiquettes",
"theme_custom_css_settings": "CSS personnalisé", "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 en cascade (CSS) permettent de personnaliser l'apparence d'Immich.",
"theme_settings": "Paramètres du thème", "theme_settings": "Paramètres du thème",
@@ -317,7 +312,6 @@
"trash_settings_description": "Gérer les paramètres de la corbeille", "trash_settings_description": "Gérer les paramètres de la corbeille",
"untracked_files": "Fichiers non suivis", "untracked_files": "Fichiers non suivis",
"untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, d'envois interrompus, ou d'abandons en raison d'un bug", "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, d'envois interrompus, ou d'abandons en raison d'un bug",
"user_cleanup_job": "Nettoyage des utilisateurs",
"user_delete_delay": "La suppression définitive du compte et des médias de <b>{user}</b> sera programmée dans {delay, plural, one {# jour} other {# jours}}.", "user_delete_delay": "La suppression définitive du compte et des médias de <b>{user}</b> sera programmée dans {delay, plural, one {# jour} other {# jours}}.",
"user_delete_delay_settings": "Délai de suppression", "user_delete_delay_settings": "Délai de suppression",
"user_delete_delay_settings_description": "Nombre de jours après la validation pour supprimer définitivement le compte et les médias d'un utilisateur. La suppression des utilisateurs se lance à minuit. Les modifications apportées à ce paramètre seront pris en compte lors de la prochaine exécution.", "user_delete_delay_settings_description": "Nombre de jours après la validation pour supprimer définitivement le compte et les médias d'un utilisateur. La suppression des utilisateurs se lance à minuit. Les modifications apportées à ce paramètre seront pris en compte lors de la prochaine exécution.",
@@ -494,8 +488,8 @@
"create_new_person": "Créer une nouvelle personne", "create_new_person": "Créer une nouvelle personne",
"create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne", "create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne",
"create_new_user": "Créer un nouvel utilisateur", "create_new_user": "Créer un nouvel utilisateur",
"create_tag": "Créer une étiquette", "create_tag": "Créer un tag",
"create_tag_description": "Créer une nouvelle étiquette. Pour les étiquettes imbriquées, veuillez entrer le chemin complet de l'étiquette, y compris les caractères \"/\".", "create_tag_description": "Créer un nouveau tag. Pour les tags imbriqués, veuillez entrer le chemin complet du tag, y compris les \"/\" avant.",
"create_user": "Créer un utilisateur", "create_user": "Créer un utilisateur",
"created": "Créé", "created": "Créé",
"current_device": "Appareil actuel", "current_device": "Appareil actuel",
@@ -519,8 +513,8 @@
"delete_library": "Supprimer la bibliothèque", "delete_library": "Supprimer la bibliothèque",
"delete_link": "Supprimer le lien", "delete_link": "Supprimer le lien",
"delete_shared_link": "Supprimer le lien partagé", "delete_shared_link": "Supprimer le lien partagé",
"delete_tag": "Supprimer l'étiquette", "delete_tag": "Supprimer le tag",
"delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer l'étiquette {tagName}?", "delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer le tag {tagName}?",
"delete_user": "Supprimer l'utilisateur", "delete_user": "Supprimer l'utilisateur",
"deleted_shared_link": "Lien partagé supprimé", "deleted_shared_link": "Lien partagé supprimé",
"description": "Description", "description": "Description",
@@ -569,7 +563,7 @@
"edit_location": "Modifier la localisation", "edit_location": "Modifier la localisation",
"edit_name": "Modifier le nom", "edit_name": "Modifier le nom",
"edit_people": "Modifier les personnes", "edit_people": "Modifier les personnes",
"edit_tag": "Modifier l'étiquette", "edit_tag": "Modifier le tag",
"edit_title": "Modifier le title", "edit_title": "Modifier le title",
"edit_user": "Modifier l'utilisateur", "edit_user": "Modifier l'utilisateur",
"edited": "Modifié", "edited": "Modifié",
@@ -684,8 +678,8 @@
"unable_to_remove_api_key": "Impossible de supprimer la clé API", "unable_to_remove_api_key": "Impossible de supprimer la clé API",
"unable_to_remove_assets_from_shared_link": "Impossible de supprimer des médias du lien partagé", "unable_to_remove_assets_from_shared_link": "Impossible de supprimer des médias du lien partagé",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "Impossible de supprimer les fichiers hors ligne",
"unable_to_remove_library": "Impossible de supprimer la bibliothèque", "unable_to_remove_library": "Impossible de supprimer la bibliothèque",
"unable_to_remove_offline_files": "Impossible de supprimer les fichiers hors ligne",
"unable_to_remove_partner": "Impossible de supprimer le partenaire", "unable_to_remove_partner": "Impossible de supprimer le partenaire",
"unable_to_remove_reaction": "Impossible de supprimer la réaction", "unable_to_remove_reaction": "Impossible de supprimer la réaction",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -1088,10 +1082,10 @@
"remove_assets_shared_link_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de ce lien partagé ?", "remove_assets_shared_link_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de ce lien partagé ?",
"remove_assets_title": "Supprimer les médias?", "remove_assets_title": "Supprimer les médias?",
"remove_custom_date_range": "Supprimer la plage de date personnalisée", "remove_custom_date_range": "Supprimer la plage de date personnalisée",
"remove_deleted_assets": "Supprimer les fichiers hors ligne",
"remove_from_album": "Supprimer de l'album", "remove_from_album": "Supprimer de l'album",
"remove_from_favorites": "Supprimer des favoris", "remove_from_favorites": "Supprimer des favoris",
"remove_from_shared_link": "Supprimer des liens partagés", "remove_from_shared_link": "Supprimer des liens partagés",
"remove_offline_files": "Supprimer les fichiers hors ligne",
"remove_user": "Supprimer l'utilisateur", "remove_user": "Supprimer l'utilisateur",
"removed_api_key": "Clé API supprimée: {name}", "removed_api_key": "Clé API supprimée: {name}",
"removed_from_archive": "Supprimé de l'archive", "removed_from_archive": "Supprimé de l'archive",
@@ -1147,9 +1141,8 @@
"search_options": "Rechercher une option", "search_options": "Rechercher une option",
"search_people": "Rechercher une personne", "search_people": "Rechercher une personne",
"search_places": "Rechercher un lieu", "search_places": "Rechercher un lieu",
"search_settings": "Paramètres de recherche",
"search_state": "Rechercher par état/région...", "search_state": "Rechercher par état/région...",
"search_tags": "Recherche d'étiquettes...", "search_tags": "Recherche de tags...",
"search_timezone": "Rechercher par fuseau horaire...", "search_timezone": "Rechercher par fuseau horaire...",
"search_type": "Rechercher par type", "search_type": "Rechercher par type",
"search_your_photos": "Rechercher vos photos", "search_your_photos": "Rechercher vos photos",
@@ -1225,7 +1218,7 @@
"size": "Taille", "size": "Taille",
"skip_to_content": "Passer", "skip_to_content": "Passer",
"skip_to_folders": "Passer vers les dossiers", "skip_to_folders": "Passer vers les dossiers",
"skip_to_tags": "Passer vers les étiquettes", "skip_to_tags": "Passer vers les tags",
"slideshow": "Diaporama", "slideshow": "Diaporama",
"slideshow_settings": "Paramètres du diaporama", "slideshow_settings": "Paramètres du diaporama",
"sort_albums_by": "Trier les albums par...", "sort_albums_by": "Trier les albums par...",
@@ -1260,12 +1253,12 @@
"sync": "Synchroniser", "sync": "Synchroniser",
"tag": "Tag", "tag": "Tag",
"tag_assets": "Taguer les médias", "tag_assets": "Taguer les médias",
"tag_created": "Étiquette créée: {tag}", "tag_created": "Tag créé : {tag}",
"tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques", "tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques",
"tag_not_found_question": "Vous ne trouvez pas une étiquette? Créez-en une <link>ici</link>", "tag_not_found_question": "Vous ne trouvez pas un tag? Créez-en un <link>ici</link>",
"tag_updated": "Étiquette mise à jour: {tag}", "tag_updated": "Tag mis à jour: {tag}",
"tagged_assets": "Tag ajouté à {count, plural, one {# média} other {# médias}}", "tagged_assets": "Tag ajouté à {count, plural, one {# média} other {# médias}}",
"tags": "Étiquettes", "tags": "Tags",
"template": "Modèle", "template": "Modèle",
"theme": "Thème", "theme": "Thème",
"theme_selection": "Sélection du thème", "theme_selection": "Sélection du thème",
+3 -10
View File
@@ -41,7 +41,6 @@
"confirm_email_below": "כדי לאשר, יש להקליד \"{email}\" למטה", "confirm_email_below": "כדי לאשר, יש להקליד \"{email}\" למטה",
"confirm_reprocess_all_faces": "האם את/ה בטוח/ה שברצונך לעבד מחדש את כל הפנים? זה גם ינקה אנשים בעלי שם.", "confirm_reprocess_all_faces": "האם את/ה בטוח/ה שברצונך לעבד מחדש את כל הפנים? זה גם ינקה אנשים בעלי שם.",
"confirm_user_password_reset": "האם את/ה בטוח/ה שברצונך לאפס את הסיסמה של המשתמש {user}?", "confirm_user_password_reset": "האם את/ה בטוח/ה שברצונך לאפס את הסיסמה של המשתמש {user}?",
"create_job": "צור עבודה",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "השבת כניסה", "disable_login": "השבת כניסה",
"disabled": "מושבת", "disabled": "מושבת",
@@ -71,7 +70,6 @@
"image_thumbnail_resolution": "רזולוציית תמונה ממוזערת", "image_thumbnail_resolution": "רזולוציית תמונה ממוזערת",
"image_thumbnail_resolution_description": "משמש בעת צפייה בקבוצות של תמונות (ציר זמן ראשי, תצוגת אלבום וכו'). רזולוציות גבוהות יותר יכולות לשמר פירוט רב יותר אך לוקחות יותר זמן לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.", "image_thumbnail_resolution_description": "משמש בעת צפייה בקבוצות של תמונות (ציר זמן ראשי, תצוגת אלבום וכו'). רזולוציות גבוהות יותר יכולות לשמר פירוט רב יותר אך לוקחות יותר זמן לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.",
"job_concurrency": "בו-זמניות של {job}", "job_concurrency": "בו-זמניות של {job}",
"job_created": "עבודה נוצרה",
"job_not_concurrency_safe": "משימה זו אינה בטוחה במקביל.", "job_not_concurrency_safe": "משימה זו אינה בטוחה במקביל.",
"job_settings": "הגדרות משימה", "job_settings": "הגדרות משימה",
"job_settings_description": "ניהול בו-זמניות של משימה", "job_settings_description": "ניהול בו-זמניות של משימה",
@@ -200,12 +198,11 @@
"password_settings": "סיסמת התחברות", "password_settings": "סיסמת התחברות",
"password_settings_description": "נהל הגדרות סיסמת התחברות", "password_settings_description": "נהל הגדרות סיסמת התחברות",
"paths_validated_successfully": "כל הנתיבים אומתו בהצלחה", "paths_validated_successfully": "כל הנתיבים אומתו בהצלחה",
"person_cleanup_job": "ניקוי אדם",
"quota_size_gib": "גודל מכסה (GiB)", "quota_size_gib": "גודל מכסה (GiB)",
"refreshing_all_libraries": "מרענן את כל הספריות", "refreshing_all_libraries": "מרענן את כל הספריות",
"registration": "רישום מנהל מערכת", "registration": "רישום מנהל מערכת",
"registration_description": "מכיוון שאתה המשתמש הראשון במערכת, אתה תוקצה כמנהל ואתה אחראי על משימות ניהול, ומשתמשים נוספים ייווצרו על ידך.", "registration_description": "מכיוון שאתה המשתמש הראשון במערכת, אתה תוקצה כמנהל ואתה אחראי על משימות ניהול, ומשתמשים נוספים ייווצרו על ידך.",
"removing_deleted_files": "הסרת קבצים לא מקוונים", "removing_offline_files": "הסרת קבצים לא מקוונים",
"repair_all": "תקן הכל", "repair_all": "תקן הכל",
"repair_matched_items": "{count, plural, one {פריט # תואם} other {# פריטים תואמים}}", "repair_matched_items": "{count, plural, one {פריט # תואם} other {# פריטים תואמים}}",
"repaired_items": "{count, plural, one {פריט # תוקן} other {# פריטים תוקנו}}", "repaired_items": "{count, plural, one {פריט # תוקן} other {# פריטים תוקנו}}",
@@ -214,7 +211,6 @@
"reset_settings_to_recent_saved": "אפס הגדרות להגדרות שנשמרו לאחרונה", "reset_settings_to_recent_saved": "אפס הגדרות להגדרות שנשמרו לאחרונה",
"scanning_library_for_changed_files": "סורק ספרייה לאיתור קבצים שהשתנו", "scanning_library_for_changed_files": "סורק ספרייה לאיתור קבצים שהשתנו",
"scanning_library_for_new_files": "סורק ספרייה לאיתור קבצים חדשים", "scanning_library_for_new_files": "סורק ספרייה לאיתור קבצים חדשים",
"search_jobs": "חיפוש עבודות...",
"send_welcome_email": "שלח דוא\"ל ברוכים הבאים", "send_welcome_email": "שלח דוא\"ל ברוכים הבאים",
"server_external_domain_settings": "דומיין חיצוני", "server_external_domain_settings": "דומיין חיצוני",
"server_external_domain_settings_description": "דומיין עבור קישורים משותפים ציבוריים, כולל http(s)://", "server_external_domain_settings_description": "דומיין עבור קישורים משותפים ציבוריים, כולל http(s)://",
@@ -242,7 +238,6 @@
"storage_template_settings_description": "נהל את מבנה התיקיות ואת שם הקובץ של נכס ההעלאה", "storage_template_settings_description": "נהל את מבנה התיקיות ואת שם הקובץ של נכס ההעלאה",
"storage_template_user_label": "<code>{label}</code> היא תווית האחסון של המשתמש", "storage_template_user_label": "<code>{label}</code> היא תווית האחסון של המשתמש",
"system_settings": "הגדרות מערכת", "system_settings": "הגדרות מערכת",
"tag_cleanup_job": "ניקוי תגים",
"theme_custom_css_settings": "CSS בהתאמה אישית", "theme_custom_css_settings": "CSS בהתאמה אישית",
"theme_custom_css_settings_description": "גיליונות סגנון מדורגים (CSS) מאפשרים התאמה אישית של העיצוב של Immich.", "theme_custom_css_settings_description": "גיליונות סגנון מדורגים (CSS) מאפשרים התאמה אישית של העיצוב של Immich.",
"theme_settings": "הגדרות ערכת נושא", "theme_settings": "הגדרות ערכת נושא",
@@ -317,7 +312,6 @@
"trash_settings_description": "נהל את הגדרות האשפה", "trash_settings_description": "נהל את הגדרות האשפה",
"untracked_files": "קבצים ללא מעקב", "untracked_files": "קבצים ללא מעקב",
"untracked_files_description": "קבצים אלה אינם נמצאים במעקב של היישום. הם יכולים להיות תוצאות של העברות כושלות, העלאות שנקטעו, או שנותרו מאחור בגלל שיבוש בתוכנה", "untracked_files_description": "קבצים אלה אינם נמצאים במעקב של היישום. הם יכולים להיות תוצאות של העברות כושלות, העלאות שנקטעו, או שנותרו מאחור בגלל שיבוש בתוכנה",
"user_cleanup_job": "ניקוי משתמשים",
"user_delete_delay": "החשבון והנכסים של <b>{user}</b> יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.", "user_delete_delay": "החשבון והנכסים של <b>{user}</b> יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.",
"user_delete_delay_settings": "עיכוב מחיקה", "user_delete_delay_settings": "עיכוב מחיקה",
"user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והנכסים של המשתמש. משימת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.", "user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והנכסים של המשתמש. משימת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.",
@@ -684,8 +678,8 @@
"unable_to_remove_api_key": "לא ניתן להסיר מפתח API", "unable_to_remove_api_key": "לא ניתן להסיר מפתח API",
"unable_to_remove_assets_from_shared_link": "לא ניתן להסיר נכסים מקישור משותף", "unable_to_remove_assets_from_shared_link": "לא ניתן להסיר נכסים מקישור משותף",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "לא ניתן להסיר קבצים לא מקוונים",
"unable_to_remove_library": "לא ניתן להסיר ספרייה", "unable_to_remove_library": "לא ניתן להסיר ספרייה",
"unable_to_remove_offline_files": "לא ניתן להסיר קבצים לא מקוונים",
"unable_to_remove_partner": "לא ניתן להסיר שותף", "unable_to_remove_partner": "לא ניתן להסיר שותף",
"unable_to_remove_reaction": "לא ניתן להסיר תגובה", "unable_to_remove_reaction": "לא ניתן להסיר תגובה",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -1088,10 +1082,10 @@
"remove_assets_shared_link_confirmation": "האם את/ה בטוח/ה שברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהקישור המשותף הזה?", "remove_assets_shared_link_confirmation": "האם את/ה בטוח/ה שברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהקישור המשותף הזה?",
"remove_assets_title": "הסר נכסים?", "remove_assets_title": "הסר נכסים?",
"remove_custom_date_range": "הסר טווח תאריכים מותאם", "remove_custom_date_range": "הסר טווח תאריכים מותאם",
"remove_deleted_assets": "הסר קבצים לא מקוונים",
"remove_from_album": "הסר מאלבום", "remove_from_album": "הסר מאלבום",
"remove_from_favorites": "הסר מהמועדפים", "remove_from_favorites": "הסר מהמועדפים",
"remove_from_shared_link": "הסר מקישור משותף", "remove_from_shared_link": "הסר מקישור משותף",
"remove_offline_files": "הסר קבצים לא מקוונים",
"remove_user": "הסר משתמש", "remove_user": "הסר משתמש",
"removed_api_key": "מפתח API הוסר: {name}", "removed_api_key": "מפתח API הוסר: {name}",
"removed_from_archive": "הוסר מארכיון", "removed_from_archive": "הוסר מארכיון",
@@ -1147,7 +1141,6 @@
"search_options": "אפשרויות חיפוש", "search_options": "אפשרויות חיפוש",
"search_people": "חפש אנשים", "search_people": "חפש אנשים",
"search_places": "חפש מקומות", "search_places": "חפש מקומות",
"search_settings": "הגדרות חיפוש",
"search_state": "חפש מדינה...", "search_state": "חפש מדינה...",
"search_tags": "חיפוש תגים...", "search_tags": "חיפוש תגים...",
"search_timezone": "חפש אזור זמן...", "search_timezone": "חפש אזור זמן...",
+3 -3
View File
@@ -197,7 +197,7 @@
"refreshing_all_libraries": "सभी पुस्तकालयों को ताज़ा किया जा रहा है", "refreshing_all_libraries": "सभी पुस्तकालयों को ताज़ा किया जा रहा है",
"registration": "व्यवस्थापक पंजीकरण", "registration": "व्यवस्थापक पंजीकरण",
"registration_description": "चूंकि आप सिस्टम पर पहले उपयोगकर्ता हैं, इसलिए आपको व्यवस्थापक के रूप में नियुक्त किया जाएगा और आप प्रशासनिक कार्यों के लिए जिम्मेदार होंगे, और अतिरिक्त उपयोगकर्ता आपके द्वारा बनाए जाएंगे।", "registration_description": "चूंकि आप सिस्टम पर पहले उपयोगकर्ता हैं, इसलिए आपको व्यवस्थापक के रूप में नियुक्त किया जाएगा और आप प्रशासनिक कार्यों के लिए जिम्मेदार होंगे, और अतिरिक्त उपयोगकर्ता आपके द्वारा बनाए जाएंगे।",
"removing_deleted_files": "ऑफ़लाइन फ़ाइलें हटाना", "removing_offline_files": "ऑफ़लाइन फ़ाइलें हटाना",
"repair_all": "सभी की मरम्मत", "repair_all": "सभी की मरम्मत",
"require_password_change_on_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है", "require_password_change_on_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है",
"reset_settings_to_default": "सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें", "reset_settings_to_default": "सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें",
@@ -605,8 +605,8 @@
"unable_to_remove_api_key": "API कुंजी निकालने में असमर्थ", "unable_to_remove_api_key": "API कुंजी निकालने में असमर्थ",
"unable_to_remove_assets_from_shared_link": "साझा लिंक से संपत्तियों को निकालने में असमर्थ", "unable_to_remove_assets_from_shared_link": "साझा लिंक से संपत्तियों को निकालने में असमर्थ",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ",
"unable_to_remove_library": "लाइब्रेरी हटाने में असमर्थ", "unable_to_remove_library": "लाइब्रेरी हटाने में असमर्थ",
"unable_to_remove_offline_files": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ",
"unable_to_remove_partner": "पार्टनर को हटाने में असमर्थ", "unable_to_remove_partner": "पार्टनर को हटाने में असमर्थ",
"unable_to_remove_reaction": "प्रतिक्रिया निकालने में असमर्थ", "unable_to_remove_reaction": "प्रतिक्रिया निकालने में असमर्थ",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -933,10 +933,10 @@
"remove": "निकालना", "remove": "निकालना",
"remove_assets_title": "संपत्तियाँ हटाएँ?", "remove_assets_title": "संपत्तियाँ हटाएँ?",
"remove_custom_date_range": "कस्टम दिनांक सीमा हटाएँ", "remove_custom_date_range": "कस्टम दिनांक सीमा हटाएँ",
"remove_deleted_assets": "ऑफ़लाइन फ़ाइलें हटाएँ",
"remove_from_album": "एल्बम से हटाएँ", "remove_from_album": "एल्बम से हटाएँ",
"remove_from_favorites": "पसंदीदा से निकालें", "remove_from_favorites": "पसंदीदा से निकालें",
"remove_from_shared_link": "साझा लिंक से हटाएँ", "remove_from_shared_link": "साझा लिंक से हटाएँ",
"remove_offline_files": "ऑफ़लाइन फ़ाइलें हटाएँ",
"remove_user": "उपयोगकर्ता को हटाएँ", "remove_user": "उपयोगकर्ता को हटाएँ",
"removed_from_archive": "संग्रह से हटा दिया गया", "removed_from_archive": "संग्रह से हटा दिया गया",
"removed_from_favorites": "पसंदीदा से हटाया गया", "removed_from_favorites": "पसंदीदा से हटाया गया",
+95 -182
View File
@@ -41,7 +41,6 @@
"confirm_email_below": "Za potvrdu upišite \"{email}\" ispod", "confirm_email_below": "Za potvrdu upišite \"{email}\" ispod",
"confirm_reprocess_all_faces": "Jeste li sigurni da želite ponovno obraditi sva lica? Ovo će također obrisati imenovane osobe.", "confirm_reprocess_all_faces": "Jeste li sigurni da želite ponovno obraditi sva lica? Ovo će također obrisati imenovane osobe.",
"confirm_user_password_reset": "Jeste li sigurni da želite poništiti lozinku korisnika {user}?", "confirm_user_password_reset": "Jeste li sigurni da želite poništiti lozinku korisnika {user}?",
"create_job": "Izradi zadatak",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "Onemogući prijavu", "disable_login": "Onemogući prijavu",
"duplicate_detection_job_description": "Pokrenite strojno učenje na materijalima kako biste otkrili slične slike. Oslanja se na Pametno Pretraživanje", "duplicate_detection_job_description": "Pokrenite strojno učenje na materijalima kako biste otkrili slične slike. Oslanja se na Pametno Pretraživanje",
@@ -70,7 +69,6 @@
"image_thumbnail_resolution": "Razlučivost sličica", "image_thumbnail_resolution": "Razlučivost sličica",
"image_thumbnail_resolution_description": "Koristi se prilikom pregledavanja grupa fotografija (glavna vremenska traka, prikaz albuma itd.). Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odaziv aplikacije.", "image_thumbnail_resolution_description": "Koristi se prilikom pregledavanja grupa fotografija (glavna vremenska traka, prikaz albuma itd.). Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odaziv aplikacije.",
"job_concurrency": "{job} istovremenost", "job_concurrency": "{job} istovremenost",
"job_created": "Zadatak je kreiran",
"job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.", "job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.",
"job_settings": "Postavke posla", "job_settings": "Postavke posla",
"job_settings_description": "Upravljajte istovremenošću poslova", "job_settings_description": "Upravljajte istovremenošću poslova",
@@ -93,8 +91,8 @@
"library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)", "library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)",
"library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke", "library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke",
"logging_enable_description": "Omogući zapisivanje", "logging_enable_description": "Omogući zapisivanje",
"logging_level_description": "Kada je omogućeno, koju razinu zapisivanja koristiti.", "logging_level_description": "Kada je omogućeno, koju razinu zapisavanje koristiti.",
"logging_settings": "Zapisivanje", "logging_settings": "Zapisavanje",
"machine_learning_clip_model": "CLIP model", "machine_learning_clip_model": "CLIP model",
"machine_learning_clip_model_description": "Naziv CLIP modela navedenog <link>ovdje</link>. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.", "machine_learning_clip_model_description": "Naziv CLIP modela navedenog <link>ovdje</link>. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.",
"machine_learning_duplicate_detection": "Detekcija Duplikata", "machine_learning_duplicate_detection": "Detekcija Duplikata",
@@ -140,7 +138,7 @@
"map_settings_description": "Upravljanje postavkama karte", "map_settings_description": "Upravljanje postavkama karte",
"map_style_description": "URL na style.json temu karte", "map_style_description": "URL na style.json temu karte",
"metadata_extraction_job": "Izdvoj metapodatke", "metadata_extraction_job": "Izdvoj metapodatke",
"metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS, lica i rezolucija", "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS i rezolucija",
"metadata_faces_import_setting": "Omogući uvoz lica", "metadata_faces_import_setting": "Omogući uvoz lica",
"metadata_faces_import_setting_description": "Uvezite lica iz EXIF podataka slike i sidecar datoteka", "metadata_faces_import_setting_description": "Uvezite lica iz EXIF podataka slike i sidecar datoteka",
"metadata_settings": "Postavke Metapodataka", "metadata_settings": "Postavke Metapodataka",
@@ -199,12 +197,11 @@
"password_settings": "Prijava zaporkom", "password_settings": "Prijava zaporkom",
"password_settings_description": "Upravljanje postavkama za prijavu zaporkom", "password_settings_description": "Upravljanje postavkama za prijavu zaporkom",
"paths_validated_successfully": "Sve su putanje uspješno potvrđene", "paths_validated_successfully": "Sve su putanje uspješno potvrđene",
"person_cleanup_job": "Čišćenje lica",
"quota_size_gib": "Veličina kvote (GiB)", "quota_size_gib": "Veličina kvote (GiB)",
"refreshing_all_libraries": "Osvježavanje svih biblioteka", "refreshing_all_libraries": "Osvježavanje svih biblioteka",
"registration": "Registracija administratora", "registration": "Registracija administratora",
"registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.", "registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.",
"removing_deleted_files": "Uklanjanje izvanmrežnih datoteka", "removing_offline_files": "Uklanjanje izvanmrežnih datoteka",
"repair_all": "Popravi sve", "repair_all": "Popravi sve",
"repair_matched_items": "Podudaranje {count, plural, one {# item} other {# items}}", "repair_matched_items": "Podudaranje {count, plural, one {# item} other {# items}}",
"repaired_items": "Popravljeno {count, plural, one {# item} other {# items}}", "repaired_items": "Popravljeno {count, plural, one {# item} other {# items}}",
@@ -213,7 +210,6 @@
"reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke", "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke",
"scanning_library_for_changed_files": "Skeniranje biblioteke za promijenjene datoteke", "scanning_library_for_changed_files": "Skeniranje biblioteke za promijenjene datoteke",
"scanning_library_for_new_files": "Skeniranje biblioteke za nove datoteke", "scanning_library_for_new_files": "Skeniranje biblioteke za nove datoteke",
"search_jobs": "Traži zadatke…",
"send_welcome_email": "Pošaljite email dobrodošlice", "send_welcome_email": "Pošaljite email dobrodošlice",
"server_external_domain_settings": "Vanjska domena", "server_external_domain_settings": "Vanjska domena",
"server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://", "server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://",
@@ -241,7 +237,6 @@
"storage_template_settings_description": "Upravljajte strukturom mape i nazivom datoteke učitanog sredstva", "storage_template_settings_description": "Upravljajte strukturom mape i nazivom datoteke učitanog sredstva",
"storage_template_user_label": "<code>{label}</code> je korisnička oznaka za pohranu", "storage_template_user_label": "<code>{label}</code> je korisnička oznaka za pohranu",
"system_settings": "Postavke Sustava", "system_settings": "Postavke Sustava",
"tag_cleanup_job": "Čišćenje oznaka",
"theme_custom_css_settings": "Prilagođeni CSS", "theme_custom_css_settings": "Prilagođeni CSS",
"theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.", "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.",
"theme_settings": "Postavke tema", "theme_settings": "Postavke tema",
@@ -315,7 +310,6 @@
"trash_settings_description": "Upravljanje postavkama smeća", "trash_settings_description": "Upravljanje postavkama smeća",
"untracked_files": "Nepraćene datoteke", "untracked_files": "Nepraćene datoteke",
"untracked_files_description": "Aplikacija ne prati ove datoteke. Mogu biti rezultat neuspjelih premještanja, prekinutih prijenosa ili izostale zbog pogreške", "untracked_files_description": "Aplikacija ne prati ove datoteke. Mogu biti rezultat neuspjelih premještanja, prekinutih prijenosa ili izostale zbog pogreške",
"user_cleanup_job": "Čišćenje korisnika",
"user_delete_delay": "Račun i sredstva korisnika <b>{user}</b> bit će zakazani za trajno brisanje za {delay, plural, one {# day} other {# days}}.", "user_delete_delay": "Račun i sredstva korisnika <b>{user}</b> bit će zakazani za trajno brisanje za {delay, plural, one {# day} other {# days}}.",
"user_delete_delay_settings": "Brisanje odgode", "user_delete_delay_settings": "Brisanje odgode",
"user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog računa i imovine. Posao brisanja korisnika pokreće se u ponoć kako bi se provjerili korisnici koji su spremni za brisanje. Promjene ove postavke bit će procijenjene pri sljedećem izvršavanju.", "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog računa i imovine. Posao brisanja korisnika pokreće se u ponoć kako bi se provjerili korisnici koji su spremni za brisanje. Promjene ove postavke bit će procijenjene pri sljedećem izvršavanju.",
@@ -455,7 +449,7 @@
"clear_value": "Očisti vrijednost", "clear_value": "Očisti vrijednost",
"clockwise": "U smjeru kazaljke na satu", "clockwise": "U smjeru kazaljke na satu",
"close": "Zatvori", "close": "Zatvori",
"collapse": "Sažmi", "collapse": "Sažimanje",
"collapse_all": "Sažmi sve", "collapse_all": "Sažmi sve",
"color": "Boja", "color": "Boja",
"color_theme": "Tema boja", "color_theme": "Tema boja",
@@ -924,179 +918,98 @@
"owner": "Vlasnik", "owner": "Vlasnik",
"partner": "Partner", "partner": "Partner",
"partner_can_access": "{partner} može pristupiti", "partner_can_access": "{partner} može pristupiti",
"partner_can_access_assets": "Sve vaše fotografije i videi osim onih u arhivi i smeću", "partner_can_access_assets": "",
"partner_can_access_location": "Mjesto otkuda je slika otkinuta", "partner_can_access_location": "",
"partner_sharing": "Dijeljenje s partnerom", "partner_sharing": "",
"partners": "Partneri", "partners": "",
"password": "Zaporka", "password": "",
"password_does_not_match": "Zaporka se ne podudara", "password_does_not_match": "",
"password_required": "Zaporka je obavezna", "password_required": "",
"password_reset_success": "Reset zaporke je uspješan", "password_reset_success": "",
"past_durations": { "past_durations": {
"days": "{days, plural, one {Prošli dan} few {Prošlih # dana} other {Prošlih # dana}}", "days": "",
"hours": "{hours, plural, one {Prošli sat} few {Prošla # sata} other {Prošlih # sati}}", "hours": "",
"years": "{years, plural, one {Prošle godine} few {Prošle # godine} other {Prošlih # godina}}" "years": ""
}, },
"path": "Putanja", "path": "",
"pattern": "Uzorak", "pattern": "",
"pause": "Pauza", "pause": "",
"pause_memories": "Pauziraj sjećanja", "pause_memories": "",
"paused": "Pauzirano", "paused": "",
"pending": "Na čekanju", "pending": "",
"people": "Ljudi", "people": "",
"people_edits_count": "Izmjenjeno {count, plural, one {# osoba} other {# osobe}}", "people_sidebar_description": "",
"people_feature_description": "Pregledavanje fotografija i videozapisa grupiranih po osobama", "permanent_deletion_warning": "",
"people_sidebar_description": "Prikažite poveznicu na Osobe na bočnoj traci", "permanent_deletion_warning_setting_description": "",
"permanent_deletion_warning": "Upozorenje za nepovratno brisanje", "permanently_delete": "",
"permanent_deletion_warning_setting_description": "Prikaži upozorenje prilikom trajnog brisanja sredstava", "permanently_deleted_asset": "",
"permanently_delete": "Nepovratno obriši", "photos": "",
"permanently_delete_assets_count": "Trajno izbriši {count, plural, one {datoteku} other {datoteke}}", "photos_count": "",
"permanently_delete_assets_prompt": "Da li ste sigurni da želite trajni izbrisati {count, plural, one {ovu datoteku?} other {ove <b>#</b> datoteke?}}Ovo će ih također ukloniti {count, plural, one {iz njihovog} other {iz njihovih}} albuma.", "photos_from_previous_years": "",
"permanently_deleted_asset": "Trajno izbrisano sredstvo", "pick_a_location": "",
"permanently_deleted_assets_count": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}", "place": "",
"person": "Osoba", "places": "",
"person_hidden": "{name}{hidden, select, true { (skriveno)} other {}}", "play": "",
"photo_shared_all_users": "Čini se da ste svoje fotografije podijelili sa svim korisnicima ili nemate nijednog korisnika s kojim biste ih podijelili.", "play_memories": "",
"photos": "Fotografije", "play_motion_photo": "",
"photos_and_videos": "Fotografije i videozapisi", "play_or_pause_video": "",
"photos_count": "{count, plural, one {{count, number} fotografija} few {{count, number} fotografije} other {{count, number} fotografija}}", "port": "",
"photos_from_previous_years": "Fotografije iz prethodnih godina", "preset": "",
"pick_a_location": "Odaberite lokaciju", "preview": "",
"place": "Mjesto", "previous": "",
"places": "Mjesta", "previous_memory": "",
"play": "Pokreni", "previous_or_next_photo": "",
"play_memories": "Pokreni sjećanja", "primary": "",
"play_motion_photo": "Reproduciraj Pokretnu fotografiju", "profile_picture_set": "",
"play_or_pause_video": "Reproducirajte ili pauzirajte video", "public_share": "",
"port": "Port", "reaction_options": "",
"preset": "Unaprijed postavljeno", "read_changelog": "",
"preview": "Pregled", "recent": "",
"previous": "Prethodno", "recent_searches": "",
"previous_memory": "Prethodno sjećanje", "refresh": "",
"previous_or_next_photo": "Prethodna ili sljedeća fotografija", "refreshed": "",
"primary": "Primarna (Primary)", "refreshes_every_file": "",
"privacy": "Privatnost", "remove": "",
"profile_image_of_user": "Profilna slika korisnika {user}", "remove_from_album": "",
"profile_picture_set": "Profilna slika postavljena.", "remove_from_favorites": "",
"public_album": "Javni album", "remove_from_shared_link": "",
"public_share": "Javno dijeljenje", "remove_offline_files": "",
"purchase_account_info": "Podržava softver", "removed_api_key": "",
"purchase_activated_subtitle": "Hvala što podržavate Immich i softver otvorenog koda", "rename": "",
"purchase_activated_time": "Aktivirano {date, date}", "repair": "",
"purchase_activated_title": "Vaš ključ je uspješno aktiviran", "repair_no_results_message": "",
"purchase_button_activate": "Aktiviraj", "replace_with_upload": "",
"purchase_button_buy": "Kupi", "require_password": "",
"purchase_button_buy_immich": "Kupi Immich", "require_user_to_change_password_on_first_login": "",
"purchase_button_never_show_again": "Nikad više ne prikazuj", "reset": "",
"purchase_button_reminder": "Podsjeti me za 30 dana", "reset_password": "",
"purchase_button_remove_key": "Ukloni ključ", "reset_people_visibility": "",
"purchase_button_select": "Odaberite", "restore": "",
"purchase_failed_activation": "Aktivacija nije uspjela! Provjerite svoju e-poštu za točan ključ proizvoda!", "restore_all": "",
"purchase_individual_description_1": "Za pojedinca", "restore_user": "",
"purchase_individual_description_2": "Status podržavanja", "resume": "",
"purchase_individual_title": "Pojedinačna licenca", "retry_upload": "",
"purchase_input_suggestion": "Imate ključ proizvoda? Unesite ključ ispod", "review_duplicates": "",
"purchase_license_subtitle": "Kupite Immich kako biste podržali kontinuirani razvoj usluge", "role": "",
"purchase_lifetime_description": "Doživotna kupnja", "save": "",
"purchase_option_title": "MOGUĆNOSTI KUPNJE", "saved_api_key": "",
"purchase_panel_info_1": "Za izgradnju Immicha potrebno je puno vremena i truda, a mi imamo inženjere koji rade na tome s punim radnim vremenom kako bismo ga učinili što boljim. Naša je misija da softver otvorenog koda i etička poslovna praksa postanu održivi izvor prihoda za programere i da se stvori ekosustav koji poštuje privatnost sa stvarnim alternativama eksploatacijskim uslugama u oblaku.", "saved_profile": "",
"purchase_panel_info_2": "Budući da se obvezujemo da nećemo dodavati dodatne pretplate, ova vam kupnja neće dodijeliti nikakve dodatne značajke u Immichu. Oslanjamo se na korisnike poput vas da podržimo stalni razvoj Immicha.", "saved_settings": "",
"purchase_panel_title": "Podrži projekt", "say_something": "",
"purchase_per_server": "Po serveru", "scan_all_libraries": "",
"purchase_per_user": "Po korisniku", "scan_all_library_files": "",
"purchase_remove_product_key": "Ukloni ključ proizvoda", "scan_new_library_files": "",
"purchase_remove_product_key_prompt": "Jeste li sigurni da želite ukloniti ključ proizvoda?", "scan_settings": "",
"purchase_remove_server_product_key": "Uklonite ključ proizvoda poslužitelja (Server)", "search": "",
"purchase_remove_server_product_key_prompt": "Jeste li sigurni da želite ukloniti ključ proizvoda poslužitelja (Server)?", "search_albums": "",
"purchase_server_description_1": "Za cijeli server", "search_by_context": "",
"purchase_server_description_2": "Status podupiratelja", "search_camera_make": "",
"purchase_server_title": "Poslužitelj (Server)", "search_camera_model": "",
"purchase_settings_server_activated": "Ključem proizvoda poslužitelja upravlja administrator", "search_city": "",
"rating": "Broj zvjezdica", "search_country": "",
"rating_clear": "Obriši ocjenu", "search_for_existing_person": "",
"rating_count": "{count, plural, one {# zvijezda} other {# zvijezde}}", "search_people": "",
"rating_description": "Prikaži EXIF ocjenu na info ploči", "search_places": "",
"reaction_options": "Mogućnosti reakcije",
"read_changelog": "Pročitajte Dnevnik promjena",
"reassign": "Ponovno dodijeli",
"reassigned_assets_to_existing_person": "Ponovo dodijeljeno{count, plural, one {# datoteka} other {# datoteke}} postojećoj {name, select, null {osobi} other {{name}}}",
"reassigned_assets_to_new_person": "Ponovo dodijeljeno {count, plural, one {# datoteka} other {# datoteke}} novoj osobi",
"reassing_hint": "Dodijelite odabrane datoteke postojećoj osobi",
"recent": "Nedavno",
"recent_searches": "Nedavne pretrage",
"refresh": "Osvježi",
"refresh_encoded_videos": "Osvježite kodirane videozapise",
"refresh_metadata": "Osvježi metapodatke",
"refresh_thumbnails": "Osvježi sličice",
"refreshed": "Osvježeno",
"refreshes_every_file": "Osvježava svaku datoteku",
"refreshing_encoded_video": "Osvježavanje kodiranog videa",
"refreshing_metadata": "Osvježavanje metapodataka",
"regenerating_thumbnails": "Obnavljanje sličica",
"remove": "Ukloni",
"remove_assets_album_confirmation": "Jeste li sigurni da želite ukloniti {count, plural, one {# datoteku} other {# datoteke}} iz albuma?",
"remove_assets_shared_link_confirmation": "Jeste li sigurni da želite ukloniti {count, plural, one {# datoteku} other {# datoteke}} iz ove dijeljene veze?",
"remove_assets_title": "Ukloniti datoteke?",
"remove_custom_date_range": "Ukloni prilagođeni datumski raspon",
"remove_deleted_assets": "",
"remove_from_album": "Ukloni iz albuma",
"remove_from_favorites": "Ukloni iz favorita",
"remove_from_shared_link": "Ukloni iz dijeljene poveznice",
"remove_user": "Ukloni korisnika",
"removed_api_key": "Uklonjen API ključ: {name}",
"removed_from_archive": "Uklonjeno iz arhive",
"removed_from_favorites": "Uklonjeno iz favorita",
"removed_from_favorites_count": "{count, plural, other {Uklonjeno #}} iz omiljenih",
"removed_tagged_assets": "Uklonjena oznaka iz {count, plural, one {# datoteke} other {# datoteka}}",
"rename": "Preimenuj",
"repair": "Popravi",
"repair_no_results_message": "Nepraćene datoteke i datoteke koje nedostaju pojavit će se ovdje",
"replace_with_upload": "Zamijeni s prijenosom",
"repository": "Spremište (Repository)",
"require_password": "Zahtijevaj lozinku",
"require_user_to_change_password_on_first_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi",
"reset": "Reset",
"reset_password": "Resetiraj lozinku",
"reset_people_visibility": "Poništi vidljivost ljudi",
"reset_to_default": "Vrati na zadano",
"resolve_duplicates": "Riješite duplikate",
"resolved_all_duplicates": "Razriješi sve duplikate",
"restore": "Oporavi",
"restore_all": "Oporavi sve",
"restore_user": "Vrati korisnika",
"restored_asset": "Obnovljena datoteka",
"resume": "Nastavi",
"retry_upload": "Ponovi prijenos",
"review_duplicates": "Pregledajte duplikate",
"role": "Uloga",
"role_editor": "Urednik",
"role_viewer": "Gledatelj",
"save": "Spremi",
"saved_api_key": "Spremljen API ključ",
"saved_profile": "Spremljen profil",
"saved_settings": "Spremljene postavke",
"say_something": "Reci nešto",
"scan_all_libraries": "Skeniraj sve Knjižnice",
"scan_all_library_files": "Ponovno skenirajte sve datoteke Knjižnice",
"scan_new_library_files": "Skeniraj nove datoteke Knjižnice",
"scan_settings": "Postavke skeniranja",
"scanning_for_album": "Skeniranje albuma...",
"search": "Pretraživanje",
"search_albums": "Traži albume",
"search_by_context": "Pretraživanje po kontekstu",
"search_by_filename": "Pretražujte prema nazivu datoteke ili ekstenziji",
"search_by_filename_example": "npr. IMG_1234.JPG ili PNG",
"search_camera_make": "Pretražite marku kamere...",
"search_camera_model": "Pretražite model kamere...",
"search_city": "Pretražite grad...",
"search_country": "Pretražite državu...",
"search_for_existing_person": "Potražite postojeću osobu",
"search_no_people": "Nema ljudi",
"search_no_people_named": "Nema osoba s imenom \"{name}\"",
"search_options": "Opcije pretraživanja",
"search_people": "Traži ljude",
"search_places": "Traži mjesta",
"search_settings": "Postavke pretraživanja",
"search_state": "", "search_state": "",
"search_timezone": "", "search_timezone": "",
"search_type": "", "search_type": "",
+47 -96
View File
@@ -34,14 +34,13 @@
"authentication_settings_reenable": "Az újbóli engedélyezéshez használjon egy<link>Szerver Parancsot</link>.", "authentication_settings_reenable": "Az újbóli engedélyezéshez használjon egy<link>Szerver Parancsot</link>.",
"background_task_job": "Háttérfolyamatok", "background_task_job": "Háttérfolyamatok",
"check_all": "Összes Kipiálása", "check_all": "Összes Kipiálása",
"cleared_jobs": "{job}: feladatok törölve", "cleared_jobs": "{job} munkák kitörölve",
"config_set_by_file": "A konfigurációt jelenleg egy konfigurációs fájl állítja be", "config_set_by_file": "A konfigurációt jelenleg egy konfigurációs fájl állítja be",
"confirm_delete_library": "Biztosan ki szeretné törölni a {library} képtárat?", "confirm_delete_library": "Biztosan ki szeretné törölni a {library} képtárat?",
"confirm_delete_library_assets": "Biztosan kitörli ezt a képtárat? Ez kitöröl {count, plural, one {#} other {#}} benne lévő fájlt az Immichből és nem visszavonható. A fájlok a lemezen maradnak.", "confirm_delete_library_assets": "Biztosan kitörli ezt a képtárat? Ez kitöröl {count, plural, one {#} other {#}} benne lévő fájlt az Immichből és nem visszavonható. A fájlok a lemezen maradnak.",
"confirm_email_below": "A megerősítéshez írja \"{email}\"-t alább", "confirm_email_below": "A megerősítéshez írja \"{email}\"-t alább",
"confirm_reprocess_all_faces": "Biztos benne, hogy újra szeretné feldolgozni az összes arcot? Ez a megnevezett személyeket is törli.", "confirm_reprocess_all_faces": "Biztos benne, hogy újra szeretné feldolgozni az összes arcot? Ez a megnevezett személyeket is törli.",
"confirm_user_password_reset": "Biztosan vissza szeretné állítani {user} jelszavát?", "confirm_user_password_reset": "Biztosan vissza szeretné állítani {user} jelszavát?",
"create_job": "Feladat létrehozása",
"crontab_guru": "Crontab Guru", "crontab_guru": "Crontab Guru",
"disable_login": "Belépés letiltása", "disable_login": "Belépés letiltása",
"disabled": "Letiltva", "disabled": "Letiltva",
@@ -71,7 +70,6 @@
"image_thumbnail_resolution": "Bélyegkép felbontás", "image_thumbnail_resolution": "Bélyegkép felbontás",
"image_thumbnail_resolution_description": "Képek csoportosított nézetekor használatos (idővonal, album nézet stb). Nagyobb felbontás esetén a kép részletgazdagabb marad, de tovább tart elkészíteni, nagyobb fájl méretet eredményes, és ronthatja az alkalmazás reagálását.", "image_thumbnail_resolution_description": "Képek csoportosított nézetekor használatos (idővonal, album nézet stb). Nagyobb felbontás esetén a kép részletgazdagabb marad, de tovább tart elkészíteni, nagyobb fájl méretet eredményes, és ronthatja az alkalmazás reagálását.",
"job_concurrency": "{job} párhuzamosság", "job_concurrency": "{job} párhuzamosság",
"job_created": "Feladat létrehozva",
"job_not_concurrency_safe": "Ez a feladat nem párhuzamosság-biztos.", "job_not_concurrency_safe": "Ez a feladat nem párhuzamosság-biztos.",
"job_settings": "Feladat beállítások", "job_settings": "Feladat beállítások",
"job_settings_description": "Feladatok párhuzamosságának beállítása", "job_settings_description": "Feladatok párhuzamosságának beállítása",
@@ -98,8 +96,8 @@
"logging_settings": "Naplózás", "logging_settings": "Naplózás",
"machine_learning_clip_model": "CLIP modell", "machine_learning_clip_model": "CLIP modell",
"machine_learning_clip_model_description": "Egy CLIP modell neve az <link>itt</link> felsoroltak közül. A modell megváltoztatása után újra kell futtatni az 'Okos Keresés' munkát minden képre.", "machine_learning_clip_model_description": "Egy CLIP modell neve az <link>itt</link> felsoroltak közül. A modell megváltoztatása után újra kell futtatni az 'Okos Keresés' munkát minden képre.",
"machine_learning_duplicate_detection": "Duplikáltak Észlelése", "machine_learning_duplicate_detection": "Másolatok Észlelése",
"machine_learning_duplicate_detection_enabled": "Duplikáltak keresésének engedélyezése", "machine_learning_duplicate_detection_enabled": "Másolatkeresés engedélyezése",
"machine_learning_duplicate_detection_enabled_description": "Ha ki van kapcsolva, a pontosan azonos fájlok akkor sem lesznek duplikálva.", "machine_learning_duplicate_detection_enabled_description": "Ha ki van kapcsolva, a pontosan azonos fájlok akkor sem lesznek duplikálva.",
"machine_learning_duplicate_detection_setting_description": "CLIP beágyazások használata a valószínű másolatok kereséséhez", "machine_learning_duplicate_detection_setting_description": "CLIP beágyazások használata a valószínű másolatok kereséséhez",
"machine_learning_enabled": "Gépi tanulás engedélyezése", "machine_learning_enabled": "Gépi tanulás engedélyezése",
@@ -109,7 +107,7 @@
"machine_learning_facial_recognition_model": "Arcfelismerési modell", "machine_learning_facial_recognition_model": "Arcfelismerési modell",
"machine_learning_facial_recognition_model_description": "A modellek méret szerint csökkenő sorrendben vannak felsorolva. A nagyobb modellek lassabbak és több memóriát használnak, de jobb eredményt produkálnak. Modellváltás után az összes képen újra le kell futtatni az arcfelismerési feladatot.", "machine_learning_facial_recognition_model_description": "A modellek méret szerint csökkenő sorrendben vannak felsorolva. A nagyobb modellek lassabbak és több memóriát használnak, de jobb eredményt produkálnak. Modellváltás után az összes képen újra le kell futtatni az arcfelismerési feladatot.",
"machine_learning_facial_recognition_setting": "Arckeresés engedélyezése", "machine_learning_facial_recognition_setting": "Arckeresés engedélyezése",
"machine_learning_facial_recognition_setting_description": "Ha ki van kapcsolva, a képek nem lesznek az arcfelismerésen lefuttatva és a Böngészés oldalon az Személyek szekcióban nem fog szerepelni senki.", "machine_learning_facial_recognition_setting_description": "Ha ki van kapcsolva, a képek nem lesznek az arcfelismerésen lefuttatva és a Felfedezés oldalon az Személyek szekcióban nem fog szerepelni senki.",
"machine_learning_max_detection_distance": "Maximum észlelési távolság", "machine_learning_max_detection_distance": "Maximum észlelési távolság",
"machine_learning_max_detection_distance_description": "Két kép közötti maximális távolság, amely esetében még másolatnak tekintjük őket (0.001 és 0.1 közötti érték). Magasabb értékek több másolatot találnak meg, de a hamis találatok esélye is nagyobb.", "machine_learning_max_detection_distance_description": "Két kép közötti maximális távolság, amely esetében még másolatnak tekintjük őket (0.001 és 0.1 közötti érték). Magasabb értékek több másolatot találnak meg, de a hamis találatok esélye is nagyobb.",
"machine_learning_max_recognition_distance": "Maximum felismerési távolság", "machine_learning_max_recognition_distance": "Maximum felismerési távolság",
@@ -140,14 +138,11 @@
"map_settings": "Térkép", "map_settings": "Térkép",
"map_settings_description": "Térkép beállítások kezelése", "map_settings_description": "Térkép beállítások kezelése",
"map_style_description": "Egy style.json térképstílusra mutató URL", "map_style_description": "Egy style.json térképstílusra mutató URL",
"metadata_extraction_job": "Metaadatok kinyerése", "metadata_extraction_job": "Metaadatok feldolgozása",
"metadata_extraction_job_description": "Metaadat-információk kinyerése minden fájlból, például GPS, arcok és felbontás", "metadata_extraction_job_description": "Metaadat-információk kinyerése minden tartalomból, például GPS, arcok és felbontás",
"metadata_faces_import_setting": "Arc importálás engedélyezése",
"metadata_faces_import_setting_description": "Arcok importálása a kép Exif adatából és metaadat fájlokból",
"metadata_settings": "Metaadat beállítások", "metadata_settings": "Metaadat beállítások",
"metadata_settings_description": "Metaadat-beállítások kezelése", "migration_job": "Migráció",
"migration_job": "Migrálás", "migration_job_description": "Az képi vagyon és arcok bélyegképeinek migrálása a legújabb mappastruktúrába",
"migration_job_description": "A fájlok és arcok bélyegképeinek migrálása a legújabb mappastruktúrába",
"no_paths_added": "Nincs megadva elérési útvonal", "no_paths_added": "Nincs megadva elérési útvonal",
"no_pattern_added": "Nincs megadva illesztési minta (pattern)", "no_pattern_added": "Nincs megadva illesztési minta (pattern)",
"note_apply_storage_label_previous_assets": "Megjegyzés: Tárolási Cimkék már korábban feltöltött képi vagyonra ragasztásához futtasd a következőt -", "note_apply_storage_label_previous_assets": "Megjegyzés: Tárolási Cimkék már korábban feltöltött képi vagyonra ragasztásához futtasd a következőt -",
@@ -180,7 +175,7 @@
"oauth_issuer_url": "Kibocsátó URL", "oauth_issuer_url": "Kibocsátó URL",
"oauth_mobile_redirect_uri": "Mobil átirányítási URI", "oauth_mobile_redirect_uri": "Mobil átirányítási URI",
"oauth_mobile_redirect_uri_override": "Mobil átirányítási URI felülírás", "oauth_mobile_redirect_uri_override": "Mobil átirányítási URI felülírás",
"oauth_mobile_redirect_uri_override_description": "Engedélyezze, ha az OAuth szolgáltató tiltja a mobil URI-t, mint például '{callback}'", "oauth_mobile_redirect_uri_override_description": "Engedélyezze, ha az 'app.immich:/' érvénytelen átirányítási URI.",
"oauth_profile_signing_algorithm": "Profil aláíró algoritmus", "oauth_profile_signing_algorithm": "Profil aláíró algoritmus",
"oauth_profile_signing_algorithm_description": "A felhasználói profil aláírásához használt algoritmus.", "oauth_profile_signing_algorithm_description": "A felhasználói profil aláírásához használt algoritmus.",
"oauth_scope": "Hatókör", "oauth_scope": "Hatókör",
@@ -200,12 +195,11 @@
"password_settings": "Jelszavas Bejelentkezés", "password_settings": "Jelszavas Bejelentkezés",
"password_settings_description": "Jelszavas bejelentkezés beállítások kezelése", "password_settings_description": "Jelszavas bejelentkezés beállítások kezelése",
"paths_validated_successfully": "Összes útvonal sikeresen érvényesítve", "paths_validated_successfully": "Összes útvonal sikeresen érvényesítve",
"person_cleanup_job": "Személy törlése",
"quota_size_gib": "Kvóta Mérete (GiB)", "quota_size_gib": "Kvóta Mérete (GiB)",
"refreshing_all_libraries": "Összes képtár újratöltése", "refreshing_all_libraries": "Összes képtár újratöltése",
"registration": "Admin Regisztráció", "registration": "Admin Regisztráció",
"registration_description": "Mivel ez az első felhasználó a rendszerben, ez a felhasználó lesz az Admin és lesz felelős adminisztratív teendőkért, illetve további felhasználókat ő tud létrehozni.", "registration_description": "Mivel ez az első felhasználó a rendszerben, ez a felhasználó lesz az Admin és lesz felelős adminisztratív teendőkért, illetve további felhasználókat ő tud létrehozni.",
"removing_deleted_files": "Offline Fájlok eltávolítása", "removing_offline_files": "Offline Fájlok eltávolítása",
"repair_all": "Összes Javítása", "repair_all": "Összes Javítása",
"repair_matched_items": "{count, plural, one {# egyezés} other {# egyezés}}", "repair_matched_items": "{count, plural, one {# egyezés} other {# egyezés}}",
"repaired_items": "Javítva {count, plural, one {# fájl} other {# fájl}}", "repaired_items": "Javítva {count, plural, one {# fájl} other {# fájl}}",
@@ -214,7 +208,6 @@
"reset_settings_to_recent_saved": "Beállítások visszaállítása a legutóbb mentettre", "reset_settings_to_recent_saved": "Beállítások visszaállítása a legutóbb mentettre",
"scanning_library_for_changed_files": "Képtár átfésülése megváltozott fájlok után", "scanning_library_for_changed_files": "Képtár átfésülése megváltozott fájlok után",
"scanning_library_for_new_files": "Képtár átfésülése új fájlok után", "scanning_library_for_new_files": "Képtár átfésülése új fájlok után",
"search_jobs": "Feladat keresés...",
"send_welcome_email": "Üdvözlő email küldése", "send_welcome_email": "Üdvözlő email küldése",
"server_external_domain_settings": "Külső domain", "server_external_domain_settings": "Külső domain",
"server_external_domain_settings_description": "Nyilvánosan megosztott linkek domainje (http(s)://-sel)", "server_external_domain_settings_description": "Nyilvánosan megosztott linkek domainje (http(s)://-sel)",
@@ -222,10 +215,10 @@
"server_settings_description": "Szerver beállítások kezelése", "server_settings_description": "Szerver beállítások kezelése",
"server_welcome_message": "Üdvözlő üzenet", "server_welcome_message": "Üdvözlő üzenet",
"server_welcome_message_description": "A bejelentkezőoldalon megjelenő üzenet.", "server_welcome_message_description": "A bejelentkezőoldalon megjelenő üzenet.",
"sidecar_job": "Metaadat feldolgozás", "sidecar_job": "Oldalkocsi fájl metaadatok",
"sidecar_job_description": "Metaadatok keresése vagy szinkronizálása a fájlrendszer alapján", "sidecar_job_description": "Fedezze fel vagy szinkronizálja az oldalkocsi fájlokban tárolt metaadatokat a fájlrendszerből",
"slideshow_duration_description": "Az egyes képek megjelenítésének ideje másodpercben", "slideshow_duration_description": "Az egyes képek megjelenítésének ideje másodpercben",
"smart_search_job_description": "Gépi tanulás futtatása a fájlokon az okos keresés támogatásához", "smart_search_job_description": "Futtasson gépi tanulást a képi vagyonon az intelligens keresés támogatása érdekében",
"storage_template_date_time_description": "A fájl készítési időpontja lesz felhasználva az időpont információhoz", "storage_template_date_time_description": "A fájl készítési időpontja lesz felhasználva az időpont információhoz",
"storage_template_date_time_sample": "Példa időpont {date}", "storage_template_date_time_sample": "Példa időpont {date}",
"storage_template_enable_description": "Tárolási sablon motor engedélyezése", "storage_template_enable_description": "Tárolási sablon motor engedélyezése",
@@ -242,14 +235,13 @@
"storage_template_settings_description": "Kezelje a feltöltött képi vagyontárgyak mappaszerkezetét és fájlnevét", "storage_template_settings_description": "Kezelje a feltöltött képi vagyontárgyak mappaszerkezetét és fájlnevét",
"storage_template_user_label": "A felhasználó Tároló Címkéje <code>{label}</code>", "storage_template_user_label": "A felhasználó Tároló Címkéje <code>{label}</code>",
"system_settings": "Rendszerbeállítások", "system_settings": "Rendszerbeállítások",
"tag_cleanup_job": "Címke törlés",
"theme_custom_css_settings": "Egyedi CSS", "theme_custom_css_settings": "Egyedi CSS",
"theme_custom_css_settings_description": "CSS Stíluslapokkal az Immich stílusa megváltoztatható.", "theme_custom_css_settings_description": "CSS Stíluslapokkal az Immich stílusa megváltoztatható.",
"theme_settings": "Stílus Beállítások", "theme_settings": "Stílus Beállítások",
"theme_settings_description": "Kezelje az Immich webes felület testreszabását", "theme_settings_description": "Kezelje az Immich webes felület testreszabását",
"these_files_matched_by_checksum": "Ezek a fájlok egyeznek az ellenőrző összegük alapján", "these_files_matched_by_checksum": "Ezek a fájlok egyeznek az ellenőrző összegük alapján",
"thumbnail_generation_job": "Bélyegképek Generálása", "thumbnail_generation_job": "Bélyegképek Generálása",
"thumbnail_generation_job_description": "Nagy, kicsi és elmosódott bélyegképek létrehozása minden elemhez, valamint bélyegképek generálása minden személyhez", "thumbnail_generation_job_description": "Hozzon létre nagy, kicsi és elmosódott bélyegképeket minden egyes elemhez, valamint bélyegképeket minden egyes személyhez",
"transcode_policy_description": "", "transcode_policy_description": "",
"transcoding_acceleration_api": "Gyorsító API", "transcoding_acceleration_api": "Gyorsító API",
"transcoding_acceleration_api_description": "Az API, amely interakcióba lép az eszközzel az átkódolás felgyorsítása érdekében. Ez a beállítás a „legtöbb, amit megtehetünk” alapon működik: hiba esetén visszaáll a szoftveres átkódolásra. A VP9 a hardvertől függően vagy működik, vagy nem.", "transcoding_acceleration_api_description": "Az API, amely interakcióba lép az eszközzel az átkódolás felgyorsítása érdekében. Ez a beállítás a „legtöbb, amit megtehetünk” alapon működik: hiba esetén visszaáll a szoftveres átkódolásra. A VP9 a hardvertől függően vagy működik, vagy nem.",
@@ -265,11 +257,11 @@
"transcoding_accepted_video_codecs_description": "Válassza ki, mely videó kodexeket nem kell átkódolni. Csak bizonyos átkódolási szabályzatokhoz használatos.", "transcoding_accepted_video_codecs_description": "Válassza ki, mely videó kodexeket nem kell átkódolni. Csak bizonyos átkódolási szabályzatokhoz használatos.",
"transcoding_advanced_options_description": "Ezeket az opciókat a legtöbb felhasználónak nem kell módosítania", "transcoding_advanced_options_description": "Ezeket az opciókat a legtöbb felhasználónak nem kell módosítania",
"transcoding_audio_codec": "Audio kodek", "transcoding_audio_codec": "Audio kodek",
"transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb hangminőség ugyanakkora tárhelyen), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.", "transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb minőség ugyanannyi helyet foglalva), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.",
"transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat", "transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat",
"transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a <h264-link>H.264 kodekhez</h264-link>, a <hevc-link>HEVC kodekhez</hevc-link> és a <vp9-link>VP9 kodekhez</vp9-link>.", "transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a <h264-link>H.264 kodekhez</h264-link>, a <hevc-link>HEVC kodekhez</hevc-link> és a <vp9-link>VP9 kodekhez</vp9-link>.",
"transcoding_constant_quality_mode": "Állandó minőségi mód", "transcoding_constant_quality_mode": "Állandó minőségi mód",
"transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont az előbbit nem minden hardver támogatja. A rendszer az itt beállított módot preferálja a minőség orientált enkódoláshoz. Az NVENC nem használja ezt a beállítást, mivel nem támogatja az ICQ-t.", "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont nem minden hardver támogatja. A rendszer az itt beállított módszert részesíti előnyben. A NVENC ignorálja a beállítást, mivel nem támogatja az ICQ-t.",
"transcoding_constant_rate_factor": "Állandó ráta tényező (-crf)", "transcoding_constant_rate_factor": "Állandó ráta tényező (-crf)",
"transcoding_constant_rate_factor_description": "Videó minőségi szint. Jellemző értékek kodekenként: H.264: 23, HEVC: 28, VP9: 31, AV1: 35. Minél alacsonyabb, annál jobb minőséget eredményez, viszont nagyobb fájlmérettel is jár.", "transcoding_constant_rate_factor_description": "Videó minőségi szint. Jellemző értékek kodekenként: H.264: 23, HEVC: 28, VP9: 31, AV1: 35. Minél alacsonyabb, annál jobb minőséget eredményez, viszont nagyobb fájlmérettel is jár.",
"transcoding_disabled_description": "Ne transzkódoljon videót. Nem lejátszható videókhoz vezethet néhány kliensen", "transcoding_disabled_description": "Ne transzkódoljon videót. Nem lejátszható videókhoz vezethet néhány kliensen",
@@ -295,7 +287,7 @@
"transcoding_settings": "Videó Transzkódolási Beállítások", "transcoding_settings": "Videó Transzkódolási Beállítások",
"transcoding_settings_description": "Videófájlok felbontásának és kódólásának kezelése", "transcoding_settings_description": "Videófájlok felbontásának és kódólásának kezelése",
"transcoding_target_resolution": "Célfelbontás", "transcoding_target_resolution": "Célfelbontás",
"transcoding_target_resolution_description": "Magasabb felbontás jobb minőségben őrzi meg a részleteket, de tovább tart kódolni, nagyobb fájlmérethez vezet, és csökkentheti az alkalmazás válaszidejét.", "transcoding_target_resolution_description": "Magasabb felbontás jobb minőségben őrzi meg a részleteket, de tovább tart kódolni, nagyobb fájlmérethez vezet, és csökkentheti az alkalmazás teljesítményét.",
"transcoding_temporal_aq": "Időbeli (Temporal) AQ", "transcoding_temporal_aq": "Időbeli (Temporal) AQ",
"transcoding_temporal_aq_description": "Csak NVENC esetén. Növeli a nagyon részletes, keveset mozgó videóanyag minőségét. Nem minden régi hardver támogatja.", "transcoding_temporal_aq_description": "Csak NVENC esetén. Növeli a nagyon részletes, keveset mozgó videóanyag minőségét. Nem minden régi hardver támogatja.",
"transcoding_threads": "Folyamatok száma", "transcoding_threads": "Folyamatok száma",
@@ -307,17 +299,16 @@
"transcoding_transcode_policy": "Transzkódolási szabályzat", "transcoding_transcode_policy": "Transzkódolási szabályzat",
"transcoding_transcode_policy_description": "Mely videókat transzkódolja. HDR videók mindig transzkódolásra kerülnek (kivéve, ha a transzkódolás ki van kapcsolva).", "transcoding_transcode_policy_description": "Mely videókat transzkódolja. HDR videók mindig transzkódolásra kerülnek (kivéve, ha a transzkódolás ki van kapcsolva).",
"transcoding_two_pass_encoding": "Enkódolás két menetben", "transcoding_two_pass_encoding": "Enkódolás két menetben",
"transcoding_two_pass_encoding_setting_description": "Ha két menetben lettek transzkódolva, az elkészült videók jobb minőségűek. Ha engedélyezve van a bitráta maximalizálása (amely szükséges a H.264 és a HEVC használatakor), ez a funkció figyelmen kívül hagyja a CRF-et. VP9 használata esetén a CRF használható, ha a bitráta nincs maximalizálva (ki van kapcsolva).", "transcoding_two_pass_encoding_setting_description": "Ha két menetben lettek transzkódolva, az elkészült videok jobbak. Ha engedélyezve van a bitráta maximalizálása (amely egyébként szükséges a H.264 és a HEVC használatakor), ez a funkció figyelmen kívül hagyja a CRF-et és a maximális bitráta alapján választja ki a megfelelő bitráta sávot. VP9 használata esetén CRF használható, ha a bitráta nincs maxmalizáva (ki van kapcsolva).",
"transcoding_video_codec": "Videó Kodek", "transcoding_video_codec": "Videó Kodek",
"transcoding_video_codec_description": "VP9 hatékonyabb és kompatibilisebb webre, de tovább tart a transzkódolás. HEVC hasonló teljesítményű, de több web kompatibilitási problémát okozhat. H.264 széles körben kompatibilis és gyors a transzkódolása, de sokkal nagyobb fájlokat készít. AV1 a leghatékonyabb kodek, de régebbi eszközök nem támogatják.", "transcoding_video_codec_description": "VP9 hatékonyabb és kompatibilisebb webre, de tovább tart a transzkódolás. HEVC hasonló teljesítményű, de több web kompatibilitási problémát okozhat. H.264 széles körben kompatibilis és gyors a transzkódolása, de sokkal nagyobb fájlokat készít. AV1 a leghatékonyabb kodek, de régebbi eszközök nem támogatják.",
"trash_enabled_description": "Lomtár engedélyezése", "trash_enabled_description": "Lomtár engedélyezése",
"trash_number_of_days": "Napok száma", "trash_number_of_days": "Napok száma",
"trash_number_of_days_description": "Hány napig legyenek a lomtárban a fájlok a végleges törlés előtt", "trash_number_of_days_description": "Hány napig legyenek a lomtárban tárolva a törölt képek, videok, mielőtt véglegesen kiürítődnek",
"trash_settings": "Lomtár Beállítások", "trash_settings": "Lomtár Beállítások",
"trash_settings_description": "Lomtár beállítások kezelése", "trash_settings_description": "Lomtár beállítások kezelése",
"untracked_files": "Nem kezelt fájlok", "untracked_files": "Nem kezelt fájlok",
"untracked_files_description": "Ezekkel a fájlokkal semmit nem csinál az alkalmazás. Ez lehetséges pl. meghiúsult mozgatás, megszakított feltöltés miatt, vagy valamilyen alkalmazáshiba következtében", "untracked_files_description": "Ezekkel a fájlokkal semmit nem csinál az alkalmazás. Ez lehetséges pl. meghiúsult mozgatás, megszakított feltöltés miatt, vagy valamilyen alkalmazáshiba következtében",
"user_cleanup_job": "Felhasználó adatainak törlése",
"user_delete_delay": "<b>{user}</b> felhasználói fiókja és képi vagyona véglegesen törölve lesz {delay, plural, one {# nap} other {# nap}} múlva.", "user_delete_delay": "<b>{user}</b> felhasználói fiókja és képi vagyona véglegesen törölve lesz {delay, plural, one {# nap} other {# nap}} múlva.",
"user_delete_delay_settings": "Törlési késleltetés", "user_delete_delay_settings": "Törlési késleltetés",
"user_delete_delay_settings_description": "Ennyi nap teljen el az eltávolítás után a felhasználói fiók és ahhoz tartozó elemek végleges törlése között. A törlésért felelős folyamat éjfélkor indul, és megnézi van-e törlésre kész felhasználó. A beállítás változtatása a következő végrehajtás során lép életbe.", "user_delete_delay_settings_description": "Ennyi nap teljen el az eltávolítás után a felhasználói fiók és ahhoz tartozó elemek végleges törlése között. A törlésért felelős folyamat éjfélkor indul, és megnézi van-e törlésre kész felhasználó. A beállítás változtatása a következő végrehajtás során lép életbe.",
@@ -348,7 +339,7 @@
"album_added": "Albumhoz hozzáadva", "album_added": "Albumhoz hozzáadva",
"album_added_notification_setting_description": "Küldjön emailes értesítőt, amikor hozzáadnak egy megosztott albumhoz", "album_added_notification_setting_description": "Küldjön emailes értesítőt, amikor hozzáadnak egy megosztott albumhoz",
"album_cover_updated": "Album borító frissítve", "album_cover_updated": "Album borító frissítve",
"album_delete_confirmation": "Biztos, hogy ki szeretné törölni a(z) {album} albumot?", "album_delete_confirmation": "Biztos, hogy ki szeretné törölni a {album} albumot?",
"album_delete_confirmation_description": "Amennyiben ez egy megosztott album, a többi felhasználó sem fog tudni hozzáférni.", "album_delete_confirmation_description": "Amennyiben ez egy megosztott album, a többi felhasználó sem fog tudni hozzáférni.",
"album_info_updated": "Album infó frissítve", "album_info_updated": "Album infó frissítve",
"album_leave": "Elhagyja az albumot?", "album_leave": "Elhagyja az albumot?",
@@ -385,7 +376,7 @@
"archive_size": "Archívum mérete", "archive_size": "Archívum mérete",
"archive_size_description": "Beállítja letöltésnél az archívum méretét (GiB)", "archive_size_description": "Beállítja letöltésnél az archívum méretét (GiB)",
"archived": "Archíválva", "archived": "Archíválva",
"archived_count": "{count, plural, other {Archiválva #}}", "archived_count": "{count, plural, other {Archived #}}",
"are_these_the_same_person": "Ugyanaz a személy?", "are_these_the_same_person": "Ugyanaz a személy?",
"are_you_sure_to_do_this": "Biztosan ezt akarod csinálni?", "are_you_sure_to_do_this": "Biztosan ezt akarod csinálni?",
"asset_added_to_album": "Hozzáadva az albumhoz", "asset_added_to_album": "Hozzáadva az albumhoz",
@@ -397,7 +388,6 @@
"asset_offline": "Elem offline", "asset_offline": "Elem offline",
"asset_offline_description": "Ez az elem nem elérhető. Immich nem képes elérni a file helyét. Győződjön meg az elem elérhetőségéről és szkennelje újra a könyvtárat.", "asset_offline_description": "Ez az elem nem elérhető. Immich nem képes elérni a file helyét. Győződjön meg az elem elérhetőségéről és szkennelje újra a könyvtárat.",
"asset_skipped": "Kihagyva", "asset_skipped": "Kihagyva",
"asset_skipped_in_trash": "Lomtárban",
"asset_uploaded": "Feltöltve", "asset_uploaded": "Feltöltve",
"asset_uploading": "Feltöltés...", "asset_uploading": "Feltöltés...",
"assets": "elemek", "assets": "elemek",
@@ -406,12 +396,12 @@
"assets_added_to_name_count": "{count, plural, other {# elem}} hozzáadva a(z) {hasName, select, true {<b>{name}</b>} other {új}} albumba", "assets_added_to_name_count": "{count, plural, other {# elem}} hozzáadva a(z) {hasName, select, true {<b>{name}</b>} other {új}} albumba",
"assets_count": "{count, plural, other {# elem}}", "assets_count": "{count, plural, other {# elem}}",
"assets_moved_to_trash": "{count, plural, one {# fájl} other {# fájl}} a lomtárba mozgatva", "assets_moved_to_trash": "{count, plural, one {# fájl} other {# fájl}} a lomtárba mozgatva",
"assets_moved_to_trash_count": "{count, plural, other {# elem}} lomtárba mozgatva", "assets_moved_to_trash_count": "{count, plural, other {# elem}} szemétbe mozgatva",
"assets_permanently_deleted_count": "{count, plural, other {# elem}} örökre törölve", "assets_permanently_deleted_count": "{count, plural, other {# elem}} örökre törölve",
"assets_removed_count": "{count, plural, other {# elem}} eltávolítva", "assets_removed_count": "{count, plural, other {# elem}} eltávolítva",
"assets_restore_confirmation": "Biztosan visszaállítja a lomtárban lévő elemeket? Ez a művelet nem visszavonható!", "assets_restore_confirmation": "Biztosan visszaállítja a lomtárbeli elemeket? Ez a művelet nem visszavonható!",
"assets_restored_count": "{count, plural, other {# elem}} visszaállítva", "assets_restored_count": "{count, plural, other {# elem}} visszaállítva",
"assets_trashed_count": "{count, plural, other {# elem}} lomtárba helyezve", "assets_trashed_count": "{count, plural, other {# elem}} kidobva",
"assets_were_part_of_album_count": "{count, plural, other {# elem}} már az album része volt", "assets_were_part_of_album_count": "{count, plural, other {# elem}} már az album része volt",
"authorized_devices": "Engedélyezett készülékek", "authorized_devices": "Engedélyezett készülékek",
"back": "Vissza", "back": "Vissza",
@@ -494,8 +484,6 @@
"create_new_person": "Új személy létrehozása", "create_new_person": "Új személy létrehozása",
"create_new_person_hint": "A kiválasztott képekhez új személyt rendel hozzá", "create_new_person_hint": "A kiválasztott képekhez új személyt rendel hozzá",
"create_new_user": "Új felhasználó létrehozása", "create_new_user": "Új felhasználó létrehozása",
"create_tag": "Címke létrehozása",
"create_tag_description": "Új címke létrehozása. Beágyazott címkék esetén adja meg a címke teljes elérési útvonalát, beleértve a perjeleket is.",
"create_user": "Felhasználó létrehozása", "create_user": "Felhasználó létrehozása",
"created": "Készült", "created": "Készült",
"current_device": "Ez az eszköz", "current_device": "Ez az eszköz",
@@ -519,8 +507,6 @@
"delete_library": "Képtár törlése", "delete_library": "Képtár törlése",
"delete_link": "Link törlése", "delete_link": "Link törlése",
"delete_shared_link": "Megosztott link törlése", "delete_shared_link": "Megosztott link törlése",
"delete_tag": "Címke törlése",
"delete_tag_confirmation_prompt": "Biztos, hogy törölni szeretné a {tagName} címkét?",
"delete_user": "Felhasználó törlése", "delete_user": "Felhasználó törlése",
"deleted_shared_link": "Törölt megosztott link", "deleted_shared_link": "Törölt megosztott link",
"description": "Leírás", "description": "Leírás",
@@ -569,7 +555,6 @@
"edit_location": "Hely módosítása", "edit_location": "Hely módosítása",
"edit_name": "Név módosítása", "edit_name": "Név módosítása",
"edit_people": "Személyek módosítása", "edit_people": "Személyek módosítása",
"edit_tag": "Címke szerkesztése",
"edit_title": "Cím Módosítása", "edit_title": "Cím Módosítása",
"edit_user": "Felhasználó módosítása", "edit_user": "Felhasználó módosítása",
"edited": "Módosítva", "edited": "Módosítva",
@@ -582,7 +567,7 @@
"empty": "", "empty": "",
"empty_album": "Üres Album", "empty_album": "Üres Album",
"empty_trash": "Lomtár Ürítése", "empty_trash": "Lomtár Ürítése",
"empty_trash_confirmation": "Biztosan kiüríti a lomtárat? Ezzel minden lomtárban lévő fájlt véglegesen letöröl az Immich szolgáltatásból.\nEz a művelet nem visszavonható!", "empty_trash_confirmation": "Biztosan kiüríti a lomtárat? Ezzel minden lomtárbeli fájlt véglegesen letöröl az Immich szolgáltatásból.\nEz a művelet nem visszavonható!",
"enable": "Engedélyezés", "enable": "Engedélyezés",
"enabled": "Engedélyezve", "enabled": "Engedélyezve",
"end_date": "Vég dátum", "end_date": "Vég dátum",
@@ -600,7 +585,7 @@
"cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen", "cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen",
"cant_search_people": "Emberek keresése sikertelen", "cant_search_people": "Emberek keresése sikertelen",
"cant_search_places": "Helyek keresése sikertelen", "cant_search_places": "Helyek keresése sikertelen",
"cleared_jobs": "A {job} feladatok törölve", "cleared_jobs": "A {job} munkák törölve",
"error_adding_assets_to_album": "Hiba történt az elemek albumhoz való hozzáadása során", "error_adding_assets_to_album": "Hiba történt az elemek albumhoz való hozzáadása során",
"error_adding_users_to_album": "Hiba történt a felhasználók albumhoz való hozzáadása során", "error_adding_users_to_album": "Hiba történt a felhasználók albumhoz való hozzáadása során",
"error_deleting_shared_user": "Hiba történt megosztott felhasználó törlése során", "error_deleting_shared_user": "Hiba történt megosztott felhasználó törlése során",
@@ -609,7 +594,7 @@
"error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat", "error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat",
"error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel", "error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel",
"exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.", "exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.",
"failed_job_command": "A(z) {command} parancs hibával zárult a(z) {job} feladatban", "failed_job_command": "Parancs {command} hibával zárult a {job} munkában",
"failed_to_create_album": "Album készítése sikertelen", "failed_to_create_album": "Album készítése sikertelen",
"failed_to_create_shared_link": "Megosztott link készítése sikertelen", "failed_to_create_shared_link": "Megosztott link készítése sikertelen",
"failed_to_edit_shared_link": "Megosztott link szerkesztése sikertelen", "failed_to_edit_shared_link": "Megosztott link szerkesztése sikertelen",
@@ -667,7 +652,6 @@
"unable_to_get_comments_number": "Hozzászólások számának lekérdezése sikertelen", "unable_to_get_comments_number": "Hozzászólások számának lekérdezése sikertelen",
"unable_to_get_shared_link": "Megosztott link lekérdezése sikertelen", "unable_to_get_shared_link": "Megosztott link lekérdezése sikertelen",
"unable_to_hide_person": "Személy elrejtése sikertelen", "unable_to_hide_person": "Személy elrejtése sikertelen",
"unable_to_link_motion_video": "Nem lehet a motion videót hozzákapcsolni",
"unable_to_link_oauth_account": "OAuth felhasználó csatlakoztatása sikertelen", "unable_to_link_oauth_account": "OAuth felhasználó csatlakoztatása sikertelen",
"unable_to_load_album": "Album betöltése sikertelen", "unable_to_load_album": "Album betöltése sikertelen",
"unable_to_load_asset_activity": "Elem aktivitásának betöltése sikertelen", "unable_to_load_asset_activity": "Elem aktivitásának betöltése sikertelen",
@@ -684,8 +668,8 @@
"unable_to_remove_api_key": "API kulcs eltávolítása sikertelen", "unable_to_remove_api_key": "API kulcs eltávolítása sikertelen",
"unable_to_remove_assets_from_shared_link": "Elemek eltávolítása megosztott linkből sikertelen", "unable_to_remove_assets_from_shared_link": "Elemek eltávolítása megosztott linkből sikertelen",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "Offline fájlok törlése sikertelen",
"unable_to_remove_library": "Könyvtár törlése sikertelen", "unable_to_remove_library": "Könyvtár törlése sikertelen",
"unable_to_remove_offline_files": "Offline fájlok törlése sikertelen",
"unable_to_remove_partner": "Partner eltávolítása sikertelen", "unable_to_remove_partner": "Partner eltávolítása sikertelen",
"unable_to_remove_reaction": "Reakció eltávolítása sikertelen", "unable_to_remove_reaction": "Reakció eltávolítása sikertelen",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -705,10 +689,9 @@
"unable_to_scan_library": "Könyvtár ellenőrzése sikertelen", "unable_to_scan_library": "Könyvtár ellenőrzése sikertelen",
"unable_to_set_feature_photo": "Kijelölt fénykép beállítása sikertelen", "unable_to_set_feature_photo": "Kijelölt fénykép beállítása sikertelen",
"unable_to_set_profile_picture": "Profilkép beállítása sikertelen", "unable_to_set_profile_picture": "Profilkép beállítása sikertelen",
"unable_to_submit_job": "Nem sikerült a feladatot elindítani", "unable_to_submit_job": "Nem sikerült a profilt elmenteni",
"unable_to_trash_asset": "Nem sikerült a fájl lomtárba mozgatása", "unable_to_trash_asset": "Nem sikerült a fájl lomtárba mozgatása",
"unable_to_unlink_account": "Nem sikerült a fiók lekapcsolása", "unable_to_unlink_account": "Nem sikerült a fiók lekapcsolása",
"unable_to_unlink_motion_video": "Nem lehet a motion videót leválasztani",
"unable_to_update_album_cover": "Albumborító beállítása sikertelen", "unable_to_update_album_cover": "Albumborító beállítása sikertelen",
"unable_to_update_album_info": "Album információ frissítése sikertelen", "unable_to_update_album_info": "Album információ frissítése sikertelen",
"unable_to_update_library": "Nem sikerült a képtár módosítása", "unable_to_update_library": "Nem sikerült a képtár módosítása",
@@ -728,8 +711,7 @@
"expire_after": "Lejárati idő", "expire_after": "Lejárati idő",
"expired": "Lejárt", "expired": "Lejárt",
"expires_date": "Lejár {date}", "expires_date": "Lejár {date}",
"explore": "Böngészés", "explore": "Felfedezés",
"explorer": "Böngésző",
"export": "Exportálás", "export": "Exportálás",
"export_as_json": "Exportálás JSON formátumban", "export_as_json": "Exportálás JSON formátumban",
"extension": "Kiterjesztés", "extension": "Kiterjesztés",
@@ -743,8 +725,6 @@
"feature": "", "feature": "",
"feature_photo_updated": "Címlapkép frissítve", "feature_photo_updated": "Címlapkép frissítve",
"featurecollection": "", "featurecollection": "",
"features": "Jellemzők",
"features_setting_description": "Az alkalmazás lehetőségeinek kezelése",
"file_name": "Fájlnév", "file_name": "Fájlnév",
"file_name_or_extension": "Fájlnév vagy kiterjesztés", "file_name_or_extension": "Fájlnév vagy kiterjesztés",
"filename": "Fájlnév", "filename": "Fájlnév",
@@ -753,8 +733,6 @@
"filter_people": "Személyek szűrése", "filter_people": "Személyek szűrése",
"find_them_fast": "Kereséssel gyorsan megtalálhatóak név alapján", "find_them_fast": "Kereséssel gyorsan megtalálhatóak név alapján",
"fix_incorrect_match": "Hibás találat korrigálása", "fix_incorrect_match": "Hibás találat korrigálása",
"folders": "Mappák",
"folders_feature_description": "A fájlrendszerben lévő fényképek és videók mappanézetben való böngészése",
"force_re-scan_library_files": "Az összes Képtár fájl újbóli átfésülésének indítása", "force_re-scan_library_files": "Az összes Képtár fájl újbóli átfésülésének indítása",
"forward": "Előre", "forward": "Előre",
"general": "Általános", "general": "Általános",
@@ -775,7 +753,7 @@
"hide_password": "Jelszó elrejtése", "hide_password": "Jelszó elrejtése",
"hide_person": "Személy elrejtése", "hide_person": "Személy elrejtése",
"hide_unnamed_people": "Megnevezetlen emberek elrejtése", "hide_unnamed_people": "Megnevezetlen emberek elrejtése",
"host": "Kiszolgáló", "host": "",
"hour": "Óra", "hour": "Óra",
"image": "Kép", "image": "Kép",
"image_alt_text_date": "{isVideo, select, true {Videó} other {Kép}} készítési dátuma {date}", "image_alt_text_date": "{isVideo, select, true {Videó} other {Kép}} készítési dátuma {date}",
@@ -826,7 +804,6 @@
"library_options": "Képtár beállítások", "library_options": "Képtár beállítások",
"light": "Világos", "light": "Világos",
"like_deleted": "Tetszik törölve", "like_deleted": "Tetszik törölve",
"link_motion_video": "Motion videó hozzárendelése",
"link_options": "Link beállítások", "link_options": "Link beállítások",
"link_to_oauth": "Csatlakoztatás OAuth-hoz", "link_to_oauth": "Csatlakoztatás OAuth-hoz",
"linked_oauth_account": "Csatlakoztatott OAuth felhasználó", "linked_oauth_account": "Csatlakoztatott OAuth felhasználó",
@@ -898,8 +875,8 @@
"no_assets_message": "KATTINTSON AZ ELSŐ FÉNYKÉPE FELTÖLTÉSÉHEZ", "no_assets_message": "KATTINTSON AZ ELSŐ FÉNYKÉPE FELTÖLTÉSÉHEZ",
"no_duplicates_found": "Duplikátumok nem találhatók.", "no_duplicates_found": "Duplikátumok nem találhatók.",
"no_exif_info_available": "Exif információ nem elérhető", "no_exif_info_available": "Exif információ nem elérhető",
"no_explore_results_message": "Töltsön fel több fényképet, hogy böngészhesse a gyűjteményét.", "no_explore_results_message": "Töltsön fel több fényképet, hogy felfedezze a gyűjteményét.",
"no_favorites_message": "Hozzáadás a kedvencekhez, hogy hamarabb megtalálhassa a legjobb fényképeit és videóit", "no_favorites_message": "Jelöljön meg kedvenceket, hogy gyorsan megtalálhassa legjobb fényképeit és videóit",
"no_libraries_message": "Hozzon létre külső képtárat a fényképei és videói megtekintéséhez", "no_libraries_message": "Hozzon létre külső képtárat a fényképei és videói megtekintéséhez",
"no_name": "Nincs Név", "no_name": "Nincs Név",
"no_places": "Nincsenek helyek", "no_places": "Nincsenek helyek",
@@ -962,7 +939,6 @@
"pending": "Folyamatban lévő", "pending": "Folyamatban lévő",
"people": "Személyek", "people": "Személyek",
"people_edits_count": "{count, plural, other {# személy}} szerkesztve", "people_edits_count": "{count, plural, other {# személy}} szerkesztve",
"people_feature_description": "Személyek szerint csoportosított fényképek és videók böngészése",
"people_sidebar_description": "Jelenítsen meg linket a Személyek fülhöz oldalt", "people_sidebar_description": "Jelenítsen meg linket a Személyek fülhöz oldalt",
"perform_library_tasks": "", "perform_library_tasks": "",
"permanent_deletion_warning": "Figyelmeztetés végleges törlésről", "permanent_deletion_warning": "Figyelmeztetés végleges törlésről",
@@ -1033,9 +1009,7 @@
"purchase_settings_server_activated": "A szerver termékkulcsot az admin menedzseli", "purchase_settings_server_activated": "A szerver termékkulcsot az admin menedzseli",
"range": "", "range": "",
"rating": "Értékelés csillagokkal", "rating": "Értékelés csillagokkal",
"rating_clear": "Értékelés törlése", "rating_description": "Exif értékelés megjelenítése az infópanelben",
"rating_count": "{count, plural, one {# csillag} other {# csillagok}}",
"rating_description": "Exif értékelés megjelenítése az infópanelen",
"raw": "", "raw": "",
"reaction_options": "Reakció lehetőségek", "reaction_options": "Reakció lehetőségek",
"read_changelog": "Változtatások olvasása", "read_changelog": "Változtatások olvasása",
@@ -1059,16 +1033,15 @@
"remove_assets_shared_link_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?", "remove_assets_shared_link_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?",
"remove_assets_title": "Elemek eltávolítása?", "remove_assets_title": "Elemek eltávolítása?",
"remove_custom_date_range": "Szabadon megadott időintervallum eltávolítása", "remove_custom_date_range": "Szabadon megadott időintervallum eltávolítása",
"remove_deleted_assets": "Offline Fájlok Eltávolítása",
"remove_from_album": "Eltávolítás az albumból", "remove_from_album": "Eltávolítás az albumból",
"remove_from_favorites": "Eltávolítás a kedvencekből", "remove_from_favorites": "Eltávolítás a kedvencekből",
"remove_from_shared_link": "Eltávolítás a megosztott linkből", "remove_from_shared_link": "Eltávolítás a megosztott linkből",
"remove_offline_files": "Offline Fájlok Eltávolítása",
"remove_user": "Felhasználó eltávolítása", "remove_user": "Felhasználó eltávolítása",
"removed_api_key": "API Kulcs eltávolítva: {name}", "removed_api_key": "API Kulcs eltávolítva: {name}",
"removed_from_archive": "Archívumból eltávolítva", "removed_from_archive": "Archívumból eltávolítva",
"removed_from_favorites": "Kedvencekből eltávolítva", "removed_from_favorites": "Kedvencekből eltávolítva",
"removed_from_favorites_count": "A kedvencekből el lett távolítva {count, plural, other {# elem}}", "removed_from_favorites_count": "A kedvencekből el lett távolítva {count, plural, other {# elem}}",
"removed_tagged_assets": "Címke eltávolítva az {count, plural, one {# elemről} other {# elemekről}}",
"rename": "Átnevezés", "rename": "Átnevezés",
"repair": "Javítás", "repair": "Javítás",
"repair_no_results_message": "Nem megfigyelt és hiányzó fájlok itt jelennek meg", "repair_no_results_message": "Nem megfigyelt és hiányzó fájlok itt jelennek meg",
@@ -1101,8 +1074,7 @@
"scan_all_libraries": "Minden könyvtár átnézése", "scan_all_libraries": "Minden könyvtár átnézése",
"scan_all_library_files": "Minden könyvtárbeli elem újraellenőrzése", "scan_all_library_files": "Minden könyvtárbeli elem újraellenőrzése",
"scan_new_library_files": "Ellenőrzés új könyvtárbeli elemekért", "scan_new_library_files": "Ellenőrzés új könyvtárbeli elemekért",
"scan_settings": "Szkennelési beállítások", "scan_settings": "Felfedezési beállítások",
"scanning_for_album": "Album szkennelése...",
"search": "Keresés", "search": "Keresés",
"search_albums": "Albumok keresése", "search_albums": "Albumok keresése",
"search_by_context": "Keresés kontextus alapján", "search_by_context": "Keresés kontextus alapján",
@@ -1115,16 +1087,13 @@
"search_for_existing_person": "Már meglévő személy keresése", "search_for_existing_person": "Már meglévő személy keresése",
"search_no_people": "Nincs személy", "search_no_people": "Nincs személy",
"search_no_people_named": "Nincs személy \"{name}\" néven", "search_no_people_named": "Nincs személy \"{name}\" néven",
"search_options": "Keresési lehetőségek",
"search_people": "Személyek keresése", "search_people": "Személyek keresése",
"search_places": "Helyek keresése", "search_places": "Helyek keresése",
"search_settings": "Keresési beállítások",
"search_state": "Régió keresése...", "search_state": "Régió keresése...",
"search_tags": "Címkék keresése...",
"search_timezone": "Időzóna keresése...", "search_timezone": "Időzóna keresése...",
"search_type": "Típus keresése", "search_type": "Típus keresése",
"search_your_photos": "Fotók keresése", "search_your_photos": "Fotók keresése",
"searching_locales": "Helyszín keresése...", "searching_locales": "",
"second": "Másodperc", "second": "Másodperc",
"see_all_people": "Minden személy megtekintése", "see_all_people": "Minden személy megtekintése",
"select_album_cover": "Albumborító kiválasztása", "select_album_cover": "Albumborító kiválasztása",
@@ -1138,7 +1107,7 @@
"select_library_owner": "Könyvtártulajdonos kijelölése", "select_library_owner": "Könyvtártulajdonos kijelölése",
"select_new_face": "Új arc kiválasztása", "select_new_face": "Új arc kiválasztása",
"select_photos": "Fotók választása", "select_photos": "Fotók választása",
"select_trash_all": "Minden lomtárba helyezése", "select_trash_all": "Minden szemétbe helyezése",
"selected": "Kijelölt", "selected": "Kijelölt",
"selected_count": "{count, plural, other {# kiválasztva}}", "selected_count": "{count, plural, other {# kiválasztva}}",
"send_message": "Üzenet küldése", "send_message": "Üzenet küldése",
@@ -1158,7 +1127,7 @@
"settings_saved": "Beállítások mentve", "settings_saved": "Beállítások mentve",
"share": "Megosztás", "share": "Megosztás",
"shared": "Megosztva", "shared": "Megosztva",
"shared_by": "Megosztotta", "shared_by": "Megosztva általa:",
"shared_by_user": "Megosztva {user} által", "shared_by_user": "Megosztva {user} által",
"shared_by_you": "Megosztva Ön által", "shared_by_you": "Megosztva Ön által",
"shared_from_partner": "Fényképek {partner}-tól/től", "shared_from_partner": "Fényképek {partner}-tól/től",
@@ -1189,14 +1158,10 @@
"show_supporter_badge": "Támogató jelvény", "show_supporter_badge": "Támogató jelvény",
"show_supporter_badge_description": "Támogató jelvény megjelenítése", "show_supporter_badge_description": "Támogató jelvény megjelenítése",
"shuffle": "Keverés", "shuffle": "Keverés",
"sidebar": "Oldalsáv",
"sidebar_display_description": "Nézetre mutató link megjelenítése az oldalsávban",
"sign_out": "Kilépés", "sign_out": "Kilépés",
"sign_up": "Feliratkozás", "sign_up": "Feliratkozás",
"size": "Méret", "size": "Méret",
"skip_to_content": "Ugrás a tartalomhoz", "skip_to_content": "Ugrás a tartalomhoz",
"skip_to_folders": "Ugrás a mappákra",
"skip_to_tags": "Ugrás a címkékhez",
"slideshow": "Diavetítés", "slideshow": "Diavetítés",
"slideshow_settings": "Diavetítés beállításai", "slideshow_settings": "Diavetítés beállításai",
"sort_albums_by": "Albumok rendezése...", "sort_albums_by": "Albumok rendezése...",
@@ -1229,14 +1194,6 @@
"sunrise_on_the_beach": "Napkelte a tengerparton", "sunrise_on_the_beach": "Napkelte a tengerparton",
"swap_merge_direction": "Egyesítés irányának megfordítása", "swap_merge_direction": "Egyesítés irányának megfordítása",
"sync": "Szinkronizálás", "sync": "Szinkronizálás",
"tag": "Címke",
"tag_assets": "Elemek címkézése",
"tag_created": "Létrehozott címke: {tag}",
"tag_feature_description": "Címkék szerinti fényképek és videók böngészése",
"tag_not_found_question": "Nem találja a címkét? Hozzon létre egyet <link>itt</link>",
"tag_updated": "Frissített címke: {tag}",
"tagged_assets": "Címkézett {count, plural, one {# elem} other {# elemek}}",
"tags": "Címkék",
"template": "Minta", "template": "Minta",
"theme": "Téma", "theme": "Téma",
"theme_selection": "Témaválasztás", "theme_selection": "Témaválasztás",
@@ -1248,18 +1205,17 @@
"to_change_password": "Jelszó megváltoztatása", "to_change_password": "Jelszó megváltoztatása",
"to_favorite": "Kedvenc", "to_favorite": "Kedvenc",
"to_login": "Bejelentkezés", "to_login": "Bejelentkezés",
"to_parent": "Egy szinttel feljebb", "to_trash": "Szemétbe helyezés",
"to_trash": "Lomtárba helyezés",
"toggle_settings": "Beállítások változtatása", "toggle_settings": "Beállítások változtatása",
"toggle_theme": "Sötét téma váltása", "toggle_theme": "Témaváltás",
"toggle_visibility": "Láthatóság változtatása", "toggle_visibility": "Láthatóság változtatása",
"total_usage": "Összesen használatban", "total_usage": "Összesen használatban",
"trash": "Lomtár", "trash": "Lomtár",
"trash_all": "Mindet lomtárba", "trash_all": "Mindet lomtárba",
"trash_count": "{count, number} elem lomtárba helyezése", "trash_count": "{count, number} elem szemétbe helyezése",
"trash_delete_asset": "Lomtárba helyezés/törlés", "trash_delete_asset": "Elem szemétbe helyezése / törlése",
"trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videók.", "trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videok.",
"trashed_items_will_be_permanently_deleted_after": "A lomtárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.", "trashed_items_will_be_permanently_deleted_after": "A szemeteskosárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.",
"type": "Típus", "type": "Típus",
"unarchive": "Archívumból kivétel", "unarchive": "Archívumból kivétel",
"unarchived": "Archívumból kivett", "unarchived": "Archívumból kivett",
@@ -1270,11 +1226,9 @@
"unknown_album": "Ismeretlen Album", "unknown_album": "Ismeretlen Album",
"unknown_year": "Ismeretlen év", "unknown_year": "Ismeretlen év",
"unlimited": "Korlátlan", "unlimited": "Korlátlan",
"unlink_motion_video": "Mozgókép leválasztása",
"unlink_oauth": "OAuth leválasztása", "unlink_oauth": "OAuth leválasztása",
"unlinked_oauth_account": "Leválasztott OAuth felhasználó", "unlinked_oauth_account": "Leválasztott OAuth felhasználó",
"unnamed_album": "Névtelen Album", "unnamed_album": "Névtelen Album",
"unnamed_album_delete_confirmation": "Biztosan törölni szeretné ezt az albumot?",
"unnamed_share": "Névtelen Megosztás", "unnamed_share": "Névtelen Megosztás",
"unsaved_change": "Mentés nélküli változtatás", "unsaved_change": "Mentés nélküli változtatás",
"unselect_all": "Összes kiválasztás törlése", "unselect_all": "Összes kiválasztás törlése",
@@ -1286,7 +1240,7 @@
"up_next": "Következik", "up_next": "Következik",
"updated_password": "Jelszó megváltoztatva", "updated_password": "Jelszó megváltoztatva",
"upload": "Feltöltés", "upload": "Feltöltés",
"upload_concurrency": "Párhuzamos feltöltés", "upload_concurrency": "",
"upload_errors": "Feltöltés befejezve {count, plural, other {# hibával}}, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", "upload_errors": "Feltöltés befejezve {count, plural, other {# hibával}}, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.",
"upload_progress": "Hátra van {remaining, number} - Feldolgozva {processed, number}/{total, number}", "upload_progress": "Hátra van {remaining, number} - Feldolgozva {processed, number}/{total, number}",
"upload_skipped_duplicates": "{count, plural, other {# megegyező elem}} kihagyva", "upload_skipped_duplicates": "{count, plural, other {# megegyező elem}} kihagyva",
@@ -1295,7 +1249,7 @@
"upload_status_uploaded": "Feltöltve", "upload_status_uploaded": "Feltöltve",
"upload_success": "Feltöltés sikeres, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", "upload_success": "Feltöltés sikeres, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.",
"url": "URL", "url": "URL",
"usage": "Használat", "usage": "Felhasználás",
"use_custom_date_range": "Szabadon megadott időintervallum használata", "use_custom_date_range": "Szabadon megadott időintervallum használata",
"user": "Felhasználó", "user": "Felhasználó",
"user_id": "Felhasználó azonosítója", "user_id": "Felhasználó azonosítója",
@@ -1310,8 +1264,6 @@
"validate": "Ellenőrzés", "validate": "Ellenőrzés",
"variables": "Változók", "variables": "Változók",
"version": "Verzió", "version": "Verzió",
"version_announcement_closing": "Barátod, Alex",
"version_announcement_message": "Szia barátom, van egy új verziója az alkalmazásnak. Kérjük, szánj időt a <link>verzióinformáció</link> megtekintésére, és győződj meg róla, hogy a <code>docker-compose.yml</code> és a <code>.env</code> beállítások naprakészek, hogy elkerüld a hibás konfigurációt, különösen, ha WatchTower-t vagy valami más automatikus frissítési megoldást használsz.",
"video": "Videó", "video": "Videó",
"video_hover_setting": "Bélyegkép felett lebegésnél videó indítás", "video_hover_setting": "Bélyegkép felett lebegésnél videó indítás",
"video_hover_setting_description": "Ha az egér a bélyegkép felett időzik, a bélyegkép videó lejátszása induljon el. A lejátszás az indítás ikon feletti időzéssel akkor is elindul, ha ez az opció ki van kapcsolva.", "video_hover_setting_description": "Ha az egér a bélyegkép felett időzik, a bélyegkép videó lejátszása induljon el. A lejátszás az indítás ikon feletti időzéssel akkor is elindul, ha ez az opció ki van kapcsolva.",
@@ -1321,7 +1273,6 @@
"view_album": "Album megtekintése", "view_album": "Album megtekintése",
"view_all": "Összes mutatása", "view_all": "Összes mutatása",
"view_all_users": "Minden felhasználó megtekintése", "view_all_users": "Minden felhasználó megtekintése",
"view_in_timeline": "Megtekintés az idővonalon",
"view_links": "Linkek megtekintése", "view_links": "Linkek megtekintése",
"view_next_asset": "Következő elem megtekintése", "view_next_asset": "Következő elem megtekintése",
"view_previous_asset": "Előző elem megtekintése", "view_previous_asset": "Előző elem megtekintése",
+3 -3
View File
@@ -172,7 +172,7 @@
"paths_validated_successfully": "", "paths_validated_successfully": "",
"quota_size_gib": "", "quota_size_gib": "",
"refreshing_all_libraries": "", "refreshing_all_libraries": "",
"removing_deleted_files": "", "removing_offline_files": "",
"repair_all": "", "repair_all": "",
"repair_matched_items": "", "repair_matched_items": "",
"repaired_items": "", "repaired_items": "",
@@ -486,8 +486,8 @@
"unable_to_refresh_user": "", "unable_to_refresh_user": "",
"unable_to_remove_album_users": "", "unable_to_remove_album_users": "",
"unable_to_remove_api_key": "", "unable_to_remove_api_key": "",
"unable_to_remove_deleted_assets": "",
"unable_to_remove_library": "", "unable_to_remove_library": "",
"unable_to_remove_offline_files": "",
"unable_to_remove_partner": "", "unable_to_remove_partner": "",
"unable_to_remove_reaction": "", "unable_to_remove_reaction": "",
"unable_to_repair_items": "", "unable_to_repair_items": "",
@@ -720,10 +720,10 @@
"refreshed": "", "refreshed": "",
"refreshes_every_file": "", "refreshes_every_file": "",
"remove": "", "remove": "",
"remove_deleted_assets": "",
"remove_from_album": "", "remove_from_album": "",
"remove_from_favorites": "", "remove_from_favorites": "",
"remove_from_shared_link": "", "remove_from_shared_link": "",
"remove_offline_files": "",
"removed_api_key": "", "removed_api_key": "",
"rename": "", "rename": "",
"repair": "", "repair": "",
+3 -10
View File
@@ -41,7 +41,6 @@
"confirm_email_below": "Untuk mengonfirmasi, ketik \"{email}\" di bawah", "confirm_email_below": "Untuk mengonfirmasi, ketik \"{email}\" di bawah",
"confirm_reprocess_all_faces": "Apakah Anda yakin ingin memproses semua wajah? Ini juga akan menghapus nama orang.", "confirm_reprocess_all_faces": "Apakah Anda yakin ingin memproses semua wajah? Ini juga akan menghapus nama orang.",
"confirm_user_password_reset": "Apakah Anda yakin ingin mengatur ulang kata sandi {user}?", "confirm_user_password_reset": "Apakah Anda yakin ingin mengatur ulang kata sandi {user}?",
"create_job": "Buat tugas",
"disable_login": "Nonaktifkan log masuk", "disable_login": "Nonaktifkan log masuk",
"duplicate_detection_job_description": "Jalankan pembelajaran mesin pada aset untuk mendeteksi gambar yang serupa. Bergantung pada Pencarian Pintar", "duplicate_detection_job_description": "Jalankan pembelajaran mesin pada aset untuk mendeteksi gambar yang serupa. Bergantung pada Pencarian Pintar",
"exclusion_pattern_description": "Pola pengecualian memungkinkan Anda mengabaikan berkas dan folder ketika memindai pustaka Anda. Ini berguna jika Anda memiliki folder yang berisi berkas yang tidak ingin diimpor, seperti berkas RAW.", "exclusion_pattern_description": "Pola pengecualian memungkinkan Anda mengabaikan berkas dan folder ketika memindai pustaka Anda. Ini berguna jika Anda memiliki folder yang berisi berkas yang tidak ingin diimpor, seperti berkas RAW.",
@@ -69,7 +68,6 @@
"image_thumbnail_resolution": "Resolusi gambar kecil", "image_thumbnail_resolution": "Resolusi gambar kecil",
"image_thumbnail_resolution_description": "Digunakan ketika menampilkan kelompok foto (lini masa utama, tampilan album, dll.). Resolusi yang lebih tinggi dapat menjaga lebih banyak detail tetapi memerlukan waktu lama untuk mengode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.", "image_thumbnail_resolution_description": "Digunakan ketika menampilkan kelompok foto (lini masa utama, tampilan album, dll.). Resolusi yang lebih tinggi dapat menjaga lebih banyak detail tetapi memerlukan waktu lama untuk mengode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.",
"job_concurrency": "Konkurensi {job}", "job_concurrency": "Konkurensi {job}",
"job_created": "Tugas telah dibuat",
"job_not_concurrency_safe": "Tugas ini tidak aman untuk konkurensi.", "job_not_concurrency_safe": "Tugas ini tidak aman untuk konkurensi.",
"job_settings": "Pengaturan Tugas", "job_settings": "Pengaturan Tugas",
"job_settings_description": "Kelola konkurensi tugas", "job_settings_description": "Kelola konkurensi tugas",
@@ -198,12 +196,11 @@
"password_settings": "Log Masuk Kata Sandi", "password_settings": "Log Masuk Kata Sandi",
"password_settings_description": "Kelola pengaturan log masuk kata sandi", "password_settings_description": "Kelola pengaturan log masuk kata sandi",
"paths_validated_successfully": "Semua jalur berhasil divalidasi", "paths_validated_successfully": "Semua jalur berhasil divalidasi",
"person_cleanup_job": "Pembersihan data pribadi",
"quota_size_gib": "Ukuran Kuota (GiB)", "quota_size_gib": "Ukuran Kuota (GiB)",
"refreshing_all_libraries": "Menyegarkan semua pustaka", "refreshing_all_libraries": "Menyegarkan semua pustaka",
"registration": "Pendaftaran Admin", "registration": "Pendaftaran Admin",
"registration_description": "Karena Anda merupakan pengguna pertama dalam sistem, Anda akan ditetapkan sebagai Admin dan bertanggung jawab atas tugas administratif dan pengguna tambahan akan dibuat oleh Anda.", "registration_description": "Karena Anda merupakan pengguna pertama dalam sistem, Anda akan ditetapkan sebagai Admin dan bertanggung jawab atas tugas administratif dan pengguna tambahan akan dibuat oleh Anda.",
"removing_deleted_files": "Menghapus Berkas Luring", "removing_offline_files": "Menghapus Berkas Luring",
"repair_all": "Perbaiki Semua", "repair_all": "Perbaiki Semua",
"repair_matched_items": "{count, plural, one {# item} other {# item}} dicocokkan", "repair_matched_items": "{count, plural, one {# item} other {# item}} dicocokkan",
"repaired_items": "{count, plural, one {# item} other {# item}} diperbaiki", "repaired_items": "{count, plural, one {# item} other {# item}} diperbaiki",
@@ -212,7 +209,6 @@
"reset_settings_to_recent_saved": "Atur ulang pengaturan ke pengaturan tersimpan terkini", "reset_settings_to_recent_saved": "Atur ulang pengaturan ke pengaturan tersimpan terkini",
"scanning_library_for_changed_files": "Memindai pustaka untuk berkas yang telah diubah", "scanning_library_for_changed_files": "Memindai pustaka untuk berkas yang telah diubah",
"scanning_library_for_new_files": "Memindai pustaka untuk berkas baru", "scanning_library_for_new_files": "Memindai pustaka untuk berkas baru",
"search_jobs": "Mencari tugas...",
"send_welcome_email": "Kirim surel selamat datang", "send_welcome_email": "Kirim surel selamat datang",
"server_external_domain_settings": "Domain eksternal", "server_external_domain_settings": "Domain eksternal",
"server_external_domain_settings_description": "Domain untuk tautan terbagi publik, termasuk http(s)://", "server_external_domain_settings_description": "Domain untuk tautan terbagi publik, termasuk http(s)://",
@@ -240,7 +236,6 @@
"storage_template_settings_description": "Kelola struktur folder dan nama berkas dari aset yang diunggah", "storage_template_settings_description": "Kelola struktur folder dan nama berkas dari aset yang diunggah",
"storage_template_user_label": "<code>{label}</code> adalah Label Penyimpanan pengguna", "storage_template_user_label": "<code>{label}</code> adalah Label Penyimpanan pengguna",
"system_settings": "Pengaturan Sistem", "system_settings": "Pengaturan Sistem",
"tag_cleanup_job": "Pembersihan tag",
"theme_custom_css_settings": "CSS Kustom", "theme_custom_css_settings": "CSS Kustom",
"theme_custom_css_settings_description": "CSS memungkinkan desain Immich untuk diubah.", "theme_custom_css_settings_description": "CSS memungkinkan desain Immich untuk diubah.",
"theme_settings": "Pengaturan Tema", "theme_settings": "Pengaturan Tema",
@@ -314,7 +309,6 @@
"trash_settings_description": "Kelola pengaturan sampah", "trash_settings_description": "Kelola pengaturan sampah",
"untracked_files": "Berkas yang Belum Dilacak", "untracked_files": "Berkas yang Belum Dilacak",
"untracked_files_description": "Berkas ini tidak dilacak oleh aplikasi. Mereka dapat diakibatkan oleh pemindahan gagal, pengunggahan terganggu, atau tertinggal karena oleh kutu", "untracked_files_description": "Berkas ini tidak dilacak oleh aplikasi. Mereka dapat diakibatkan oleh pemindahan gagal, pengunggahan terganggu, atau tertinggal karena oleh kutu",
"user_cleanup_job": "Pembersihan data pengguna",
"user_delete_delay": "Akun dan aset <b>{user}</b> akan dijadwalkan untuk penghapusan permanen dalam {delay, plural, one {# hari} other {# hari}}.", "user_delete_delay": "Akun dan aset <b>{user}</b> akan dijadwalkan untuk penghapusan permanen dalam {delay, plural, one {# hari} other {# hari}}.",
"user_delete_delay_settings": "Jeda penghapusan", "user_delete_delay_settings": "Jeda penghapusan",
"user_delete_delay_settings_description": "Jumlah hari setelah penghapusan untuk menghapus akun dan aset pengguna secara permanen. Tugas penghapusan pengguna berjalan pada tengah malam untuk memeriksa pengguna yang siap untuk dihapus. Perubahan pengaturan ini akan dievaluasi pada eksekusi berikutnya.", "user_delete_delay_settings_description": "Jumlah hari setelah penghapusan untuk menghapus akun dan aset pengguna secara permanen. Tugas penghapusan pengguna berjalan pada tengah malam untuk memeriksa pengguna yang siap untuk dihapus. Perubahan pengaturan ini akan dievaluasi pada eksekusi berikutnya.",
@@ -669,8 +663,8 @@
"unable_to_remove_album_users": "Tidak dapat mengeluarkan pengguna dari album", "unable_to_remove_album_users": "Tidak dapat mengeluarkan pengguna dari album",
"unable_to_remove_api_key": "Tidak dapat menghapus Kunci API", "unable_to_remove_api_key": "Tidak dapat menghapus Kunci API",
"unable_to_remove_assets_from_shared_link": "Tidak dapat menghapus aset dari tautan terbagi", "unable_to_remove_assets_from_shared_link": "Tidak dapat menghapus aset dari tautan terbagi",
"unable_to_remove_deleted_assets": "Tidak dapat menghapus berkas luring",
"unable_to_remove_library": "Tidak dapat menghapus pustaka", "unable_to_remove_library": "Tidak dapat menghapus pustaka",
"unable_to_remove_offline_files": "Tidak dapat menghapus berkas luring",
"unable_to_remove_partner": "Tidak dapat menghapus partner", "unable_to_remove_partner": "Tidak dapat menghapus partner",
"unable_to_remove_reaction": "Tidak dapat menghapus reaksi", "unable_to_remove_reaction": "Tidak dapat menghapus reaksi",
"unable_to_repair_items": "Tidak dapat memperbaiki item", "unable_to_repair_items": "Tidak dapat memperbaiki item",
@@ -1059,10 +1053,10 @@
"remove_assets_shared_link_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset} other {# aset}} dari tautan terbagi ini?", "remove_assets_shared_link_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset} other {# aset}} dari tautan terbagi ini?",
"remove_assets_title": "Hapus aset?", "remove_assets_title": "Hapus aset?",
"remove_custom_date_range": "Hapus jangka tanggal khusus", "remove_custom_date_range": "Hapus jangka tanggal khusus",
"remove_deleted_assets": "Hapus Berkas Luring",
"remove_from_album": "Hapus dari album", "remove_from_album": "Hapus dari album",
"remove_from_favorites": "Hapus dari favorit", "remove_from_favorites": "Hapus dari favorit",
"remove_from_shared_link": "Hapus dari tautan terbagi", "remove_from_shared_link": "Hapus dari tautan terbagi",
"remove_offline_files": "Hapus Berkas Luring",
"remove_user": "Keluarkan pengguna", "remove_user": "Keluarkan pengguna",
"removed_api_key": "Kunci API Dihapus: {name}", "removed_api_key": "Kunci API Dihapus: {name}",
"removed_from_archive": "Dihapus dari arsip", "removed_from_archive": "Dihapus dari arsip",
@@ -1117,7 +1111,6 @@
"search_options": "Pilihan pencarian", "search_options": "Pilihan pencarian",
"search_people": "Cari orang", "search_people": "Cari orang",
"search_places": "Cari tempat", "search_places": "Cari tempat",
"search_settings": "Pengaturan pencarian",
"search_state": "Cari negara bagian...", "search_state": "Cari negara bagian...",
"search_tags": "Cari tag...", "search_tags": "Cari tag...",
"search_timezone": "Cari zona waktu...", "search_timezone": "Cari zona waktu...",
+3 -3
View File
@@ -202,7 +202,7 @@
"refreshing_all_libraries": "Aggiorna tutte le librerie", "refreshing_all_libraries": "Aggiorna tutte le librerie",
"registration": "Registrazione amministratore", "registration": "Registrazione amministratore",
"registration_description": "Poiché sei il primo utente del sistema, sarai assegnato come Amministratore e sarai responsabile dei task amministrativi, e utenti aggiuntivi saranno creati da te.", "registration_description": "Poiché sei il primo utente del sistema, sarai assegnato come Amministratore e sarai responsabile dei task amministrativi, e utenti aggiuntivi saranno creati da te.",
"removing_deleted_files": "Cancella File Offline", "removing_offline_files": "Cancella File Offline",
"repair_all": "Ripara Tutto", "repair_all": "Ripara Tutto",
"repair_matched_items": "{count, plural, one {Rilevato # elemento} other {Rilevati # elementi}}", "repair_matched_items": "{count, plural, one {Rilevato # elemento} other {Rilevati # elementi}}",
"repaired_items": "{count, plural, one {Riparato # elemento} other {Riparati # elementi}}", "repaired_items": "{count, plural, one {Riparato # elemento} other {Riparati # elementi}}",
@@ -678,8 +678,8 @@
"unable_to_remove_api_key": "Impossibile rimuovere la chiave API", "unable_to_remove_api_key": "Impossibile rimuovere la chiave API",
"unable_to_remove_assets_from_shared_link": "Errore durante la rimozione degli assets da un link condiviso", "unable_to_remove_assets_from_shared_link": "Errore durante la rimozione degli assets da un link condiviso",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "Impossibile rimuovere i file offline",
"unable_to_remove_library": "Impossibile rimuovere libreria", "unable_to_remove_library": "Impossibile rimuovere libreria",
"unable_to_remove_offline_files": "Impossibile rimuovere i file offline",
"unable_to_remove_partner": "Impossibile rimuovere compagno", "unable_to_remove_partner": "Impossibile rimuovere compagno",
"unable_to_remove_reaction": "Impossibile rimuovere reazione", "unable_to_remove_reaction": "Impossibile rimuovere reazione",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -1081,10 +1081,10 @@
"remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?", "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?",
"remove_assets_title": "Rimuovere asset?", "remove_assets_title": "Rimuovere asset?",
"remove_custom_date_range": "Rimuovi intervallo data personalizzato", "remove_custom_date_range": "Rimuovi intervallo data personalizzato",
"remove_deleted_assets": "Rimuovi file offline",
"remove_from_album": "Rimuovere dall'album", "remove_from_album": "Rimuovere dall'album",
"remove_from_favorites": "Rimuovi dai preferiti", "remove_from_favorites": "Rimuovi dai preferiti",
"remove_from_shared_link": "Rimuovi dal link condiviso", "remove_from_shared_link": "Rimuovi dal link condiviso",
"remove_offline_files": "Rimuovi file offline",
"remove_user": "Rimuovi utente", "remove_user": "Rimuovi utente",
"removed_api_key": "Rimossa chiave API: {name}", "removed_api_key": "Rimossa chiave API: {name}",
"removed_from_archive": "Rimosso dall'archivio", "removed_from_archive": "Rimosso dall'archivio",
+3 -3
View File
@@ -198,7 +198,7 @@
"refreshing_all_libraries": "すべてのライブラリを更新", "refreshing_all_libraries": "すべてのライブラリを更新",
"registration": "管理者登録", "registration": "管理者登録",
"registration_description": "あなたはシステムの最初のユーザーであるため、管理者として割り当てられ、管理タスクを担当し、追加のユーザーはあなたによって作成されます。", "registration_description": "あなたはシステムの最初のユーザーであるため、管理者として割り当てられ、管理タスクを担当し、追加のユーザーはあなたによって作成されます。",
"removing_deleted_files": "オフライン ファイルを削除します", "removing_offline_files": "オフライン ファイルを削除します",
"repair_all": "すべてを修復", "repair_all": "すべてを修復",
"repair_matched_items": "一致: {count, plural, one {#件} other {#件}}", "repair_matched_items": "一致: {count, plural, one {#件} other {#件}}",
"repaired_items": "修復済み: {count, plural, one {#件} other {#件}}", "repaired_items": "修復済み: {count, plural, one {#件} other {#件}}",
@@ -671,8 +671,8 @@
"unable_to_remove_api_key": "API キーを削除できません", "unable_to_remove_api_key": "API キーを削除できません",
"unable_to_remove_assets_from_shared_link": "共有リンクからアセットを削除できません", "unable_to_remove_assets_from_shared_link": "共有リンクからアセットを削除できません",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "オフラインのファイルを削除できません",
"unable_to_remove_library": "ライブラリを削除できません", "unable_to_remove_library": "ライブラリを削除できません",
"unable_to_remove_offline_files": "オフラインのファイルを削除できません",
"unable_to_remove_partner": "パートナーを削除できません", "unable_to_remove_partner": "パートナーを削除できません",
"unable_to_remove_reaction": "リアクションを削除できません", "unable_to_remove_reaction": "リアクションを削除できません",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -1046,10 +1046,10 @@
"remove_assets_shared_link_confirmation": "本当にこの共有リンクから{count, plural, one {#個} other {#個}}のアセットを削除しますか?", "remove_assets_shared_link_confirmation": "本当にこの共有リンクから{count, plural, one {#個} other {#個}}のアセットを削除しますか?",
"remove_assets_title": "アセットを削除しますか?", "remove_assets_title": "アセットを削除しますか?",
"remove_custom_date_range": "カスタム日付範囲を削除", "remove_custom_date_range": "カスタム日付範囲を削除",
"remove_deleted_assets": "オフラインのファイルを削除",
"remove_from_album": "アルバムから削除", "remove_from_album": "アルバムから削除",
"remove_from_favorites": "お気に入りから削除", "remove_from_favorites": "お気に入りから削除",
"remove_from_shared_link": "共有リンクから削除", "remove_from_shared_link": "共有リンクから削除",
"remove_offline_files": "オフラインのファイルを削除",
"remove_user": "ユーザーを削除", "remove_user": "ユーザーを削除",
"removed_api_key": "削除されたAPI キー: {name}", "removed_api_key": "削除されたAPI キー: {name}",
"removed_from_archive": "アーカイブから削除されました", "removed_from_archive": "アーカイブから削除されました",
+3 -3
View File
@@ -177,7 +177,7 @@
"paths_validated_successfully": "", "paths_validated_successfully": "",
"quota_size_gib": "", "quota_size_gib": "",
"refreshing_all_libraries": "", "refreshing_all_libraries": "",
"removing_deleted_files": "", "removing_offline_files": "",
"repair_all": "", "repair_all": "",
"repair_matched_items": "", "repair_matched_items": "",
"repaired_items": "", "repaired_items": "",
@@ -493,8 +493,8 @@
"unable_to_refresh_user": "", "unable_to_refresh_user": "",
"unable_to_remove_album_users": "", "unable_to_remove_album_users": "",
"unable_to_remove_api_key": "", "unable_to_remove_api_key": "",
"unable_to_remove_deleted_assets": "",
"unable_to_remove_library": "", "unable_to_remove_library": "",
"unable_to_remove_offline_files": "",
"unable_to_remove_partner": "", "unable_to_remove_partner": "",
"unable_to_remove_reaction": "", "unable_to_remove_reaction": "",
"unable_to_repair_items": "", "unable_to_repair_items": "",
@@ -727,10 +727,10 @@
"refreshed": "", "refreshed": "",
"refreshes_every_file": "", "refreshes_every_file": "",
"remove": "", "remove": "",
"remove_deleted_assets": "",
"remove_from_album": "", "remove_from_album": "",
"remove_from_favorites": "", "remove_from_favorites": "",
"remove_from_shared_link": "", "remove_from_shared_link": "",
"remove_offline_files": "",
"removed_api_key": "", "removed_api_key": "",
"rename": "", "rename": "",
"repair": "", "repair": "",
+3 -3
View File
@@ -202,7 +202,7 @@
"refreshing_all_libraries": "모든 라이브러리 다시 스캔 중...", "refreshing_all_libraries": "모든 라이브러리 다시 스캔 중...",
"registration": "관리자 가입", "registration": "관리자 가입",
"registration_description": "첫 번째 사용자이기 때문에 관리자로 지정되었습니다. 관리 작업 및 사용자 생성이 가능합니다.", "registration_description": "첫 번째 사용자이기 때문에 관리자로 지정되었습니다. 관리 작업 및 사용자 생성이 가능합니다.",
"removing_deleted_files": "누락된 파일을 제거하는 중...", "removing_offline_files": "누락된 파일을 제거하는 중...",
"repair_all": "모두 수리", "repair_all": "모두 수리",
"repair_matched_items": "동일한 항목 {count, plural, one {#개} other {#개}}를 확인했습니다.", "repair_matched_items": "동일한 항목 {count, plural, one {#개} other {#개}}를 확인했습니다.",
"repaired_items": "항목 {count, plural, one {#개} other {#개}}를 수리했습니다.", "repaired_items": "항목 {count, plural, one {#개} other {#개}}를 수리했습니다.",
@@ -676,8 +676,8 @@
"unable_to_remove_api_key": "API 키를 삭제할 수 없습니다.", "unable_to_remove_api_key": "API 키를 삭제할 수 없습니다.",
"unable_to_remove_assets_from_shared_link": "공유 링크에서 항목을 제거할 수 없습니다.", "unable_to_remove_assets_from_shared_link": "공유 링크에서 항목을 제거할 수 없습니다.",
"unable_to_remove_comment": "", "unable_to_remove_comment": "",
"unable_to_remove_deleted_assets": "누락된 파일을 제거할 수 없습니다.",
"unable_to_remove_library": "라이브러리를 제거할 수 없습니다.", "unable_to_remove_library": "라이브러리를 제거할 수 없습니다.",
"unable_to_remove_offline_files": "누락된 파일을 제거할 수 없습니다.",
"unable_to_remove_partner": "파트너를 제거할 수 없습니다.", "unable_to_remove_partner": "파트너를 제거할 수 없습니다.",
"unable_to_remove_reaction": "반응을 제거할 수 없습니다.", "unable_to_remove_reaction": "반응을 제거할 수 없습니다.",
"unable_to_remove_user": "", "unable_to_remove_user": "",
@@ -1062,10 +1062,10 @@
"remove_assets_shared_link_confirmation": "공유 링크에서 항목 {count, plural, one {#개} other {#개}}를 제거하시겠습니까?", "remove_assets_shared_link_confirmation": "공유 링크에서 항목 {count, plural, one {#개} other {#개}}를 제거하시겠습니까?",
"remove_assets_title": "항목을 제거하시겠습니까?", "remove_assets_title": "항목을 제거하시겠습니까?",
"remove_custom_date_range": "맞춤 기간 제거", "remove_custom_date_range": "맞춤 기간 제거",
"remove_deleted_assets": "누락된 파일 제거",
"remove_from_album": "앨범에서 제거", "remove_from_album": "앨범에서 제거",
"remove_from_favorites": "즐겨찾기에서 제거", "remove_from_favorites": "즐겨찾기에서 제거",
"remove_from_shared_link": "공유 링크에서 제거", "remove_from_shared_link": "공유 링크에서 제거",
"remove_offline_files": "누락된 파일 제거",
"remove_user": "사용자 삭제", "remove_user": "사용자 삭제",
"removed_api_key": "API 키 삭제: {name}", "removed_api_key": "API 키 삭제: {name}",
"removed_from_archive": "보관함에서 제거되었습니다.", "removed_from_archive": "보관함에서 제거되었습니다.",

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