mirror of
https://github.com/immich-app/immich.git
synced 2025-07-08 10:46:48 -04:00
Merge branch 'main' of github.com:immich-app/immich into mobile/collections
This commit is contained in:
commit
f73deae77c
3
.github/labeler.yml
vendored
3
.github/labeler.yml
vendored
@ -33,3 +33,6 @@ documentation:
|
|||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- machine-learning/app/**
|
- machine-learning/app/**
|
||||||
|
|
||||||
|
changelog:translation:
|
||||||
|
- head-branch: ['^chore/translations$']
|
||||||
|
70
.github/workflows/docker.yml
vendored
70
.github/workflows/docker.yml
vendored
@ -40,6 +40,57 @@ jobs:
|
|||||||
id: should_force
|
id: should_force
|
||||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
retag_ml:
|
||||||
|
name: Re-Tag ML
|
||||||
|
needs: pre-job
|
||||||
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'false' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
suffix: ["", "-cuda", "-openvino", "-armnn"]
|
||||||
|
steps:
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
# Skip when PR from a fork
|
||||||
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Re-tag image
|
||||||
|
run: |
|
||||||
|
REGISTRY_NAME="ghcr.io"
|
||||||
|
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
|
||||||
|
TAG_OLD=main${{ matrix.suffix }}
|
||||||
|
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||||
|
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||||
|
|
||||||
|
retag_server:
|
||||||
|
name: Re-Tag Server
|
||||||
|
needs: pre-job
|
||||||
|
if: ${{ needs.pre-job.outputs.should_run_server == 'false' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
suffix: [""]
|
||||||
|
steps:
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
# Skip when PR from a fork
|
||||||
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Re-tag image
|
||||||
|
run: |
|
||||||
|
REGISTRY_NAME="ghcr.io"
|
||||||
|
REPOSITORY=${{ github.repository_owner }}/immich-server
|
||||||
|
TAG_OLD=main${{ matrix.suffix }}
|
||||||
|
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||||
|
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||||
|
|
||||||
|
|
||||||
build_and_push_ml:
|
build_and_push_ml:
|
||||||
name: Build and Push ML
|
name: Build and Push ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@ -235,9 +286,22 @@ jobs:
|
|||||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
success-check:
|
success-check-server:
|
||||||
name: Docker Build & Push Success
|
name: Docker Build & Push Server Success
|
||||||
needs: [build_and_push_ml, build_and_push_server]
|
needs: [build_and_push_server, retag_server]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Any jobs failed?
|
||||||
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
|
run: exit 1
|
||||||
|
- name: All jobs passed or skipped
|
||||||
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
|
||||||
|
success-check-ml:
|
||||||
|
name: Docker Build & Push ML Success
|
||||||
|
needs: [build_and_push_ml, retag_ml]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
|
@ -92,7 +92,7 @@ For the mobile app, you can use `https://demo.immich.app/api` for the `Server En
|
|||||||
| LivePhoto/MotionPhoto backup and playback | Yes | Yes |
|
| LivePhoto/MotionPhoto backup and playback | Yes | Yes |
|
||||||
| Support 360 degree image display | No | Yes |
|
| Support 360 degree image display | No | Yes |
|
||||||
| User-defined storage structure | Yes | Yes |
|
| User-defined storage structure | Yes | Yes |
|
||||||
| Public Sharing | No | Yes |
|
| Public Sharing | Yes | Yes |
|
||||||
| Archive and Favorites | Yes | Yes |
|
| Archive and Favorites | Yes | Yes |
|
||||||
| Global Map | Yes | Yes |
|
| Global Map | Yes | Yes |
|
||||||
| Partner Sharing | Yes | Yes |
|
| Partner Sharing | Yes | Yes |
|
||||||
|
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.17",
|
"version": "2.2.18",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.17",
|
"version": "2.2.18",
|
||||||
"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.113.1",
|
"version": "1.114.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.17",
|
"version": "2.2.18",
|
||||||
"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",
|
||||||
|
@ -98,7 +98,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
|
image: redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
|
image: redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -48,7 +48,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
|
image: docker.io/redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -27,3 +27,9 @@ If an asset is in multiple albums, `{{album}}` will be set to the name of the al
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page.
|
Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page.
|
||||||
|
|
||||||
|
If you want to store assets in album folders, but you also have assets that do not belong to any album, you can use `{{#if album}}`, `{{else}}` and `{{/if}}` to create a conditional statement. For example, the following template will store assets in album folders if they belong to an album, and in a folder named "Other/Month" if they do not belong to an album:
|
||||||
|
|
||||||
|
```
|
||||||
|
{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}
|
||||||
|
```
|
||||||
|
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.114.0",
|
||||||
|
"url": "https://v1.114.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.113.1",
|
"label": "v1.113.1",
|
||||||
"url": "https://v1.113.1.archive.immich.app"
|
"url": "https://v1.113.1.archive.immich.app"
|
||||||
|
@ -33,7 +33,7 @@ services:
|
|||||||
- 2285:3001
|
- 2285:3001
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
|
image: redis:6.2-alpine@sha256:fd1b5400ca24adc2ff77abdf00acb72c3aae85b94e43557ab2606d29a74bfa01
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
|
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.113.1",
|
"version": "1.114.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.113.1",
|
"version": "1.114.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.17",
|
"version": "2.2.18",
|
||||||
"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.113.1",
|
"version": "1.114.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.113.1",
|
"version": "1.114.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -44,7 +44,7 @@ test.describe('Shared Links', () => {
|
|||||||
test('download from a shared link', async ({ page }) => {
|
test('download from a shared link', async ({ page }) => {
|
||||||
await page.goto(`/share/${sharedLink.key}`);
|
await page.goto(`/share/${sharedLink.key}`);
|
||||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||||
await page.locator('.group').first().hover();
|
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
||||||
await page.waitForSelector('#asset-group-by-date svg');
|
await page.waitForSelector('#asset-group-by-date svg');
|
||||||
await page.getByRole('checkbox').click();
|
await page.getByRole('checkbox').click();
|
||||||
await page.getByRole('button', { name: 'Download' }).click();
|
await page.getByRole('button', { name: 'Download' }).click();
|
||||||
@ -69,4 +69,15 @@ test.describe('Shared Links', () => {
|
|||||||
await page.goto('/share/invalid');
|
await page.goto('/share/invalid');
|
||||||
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('auth on navigation from shared link to timeline', async ({ context, page }) => {
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
|
||||||
|
await page.goto(`/share/${sharedLink.key}`);
|
||||||
|
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||||
|
|
||||||
|
await page.locator('a[href="/"]').click();
|
||||||
|
await page.waitForURL('/photos');
|
||||||
|
await page.locator(`[data-asset-id="${asset.id}"]`).waitFor();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -40,11 +40,10 @@ FROM prod-cpu AS prod-openvino
|
|||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||||
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb && \
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-core_1.0.17384.11_amd64.deb && \
|
||||||
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb && \
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-opencl_1.0.17384.11_amd64.deb && \
|
||||||
wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb && \
|
wget https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/intel-opencl-icd_24.31.30508.7_amd64.deb && \
|
||||||
wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb && \
|
wget https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/libigdgmm12_22.4.1_amd64.deb && \
|
||||||
wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb && \
|
|
||||||
dpkg -i *.deb && \
|
dpkg -i *.deb && \
|
||||||
rm *.deb && \
|
rm *.deb && \
|
||||||
apt-get remove wget -yqq && \
|
apt-get remove wget -yqq && \
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.113.1"
|
version = "1.114.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 157,
|
"android.injected.version.code" => 158,
|
||||||
"android.injected.version.name" => "1.113.1",
|
"android.injected.version.name" => "1.114.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')
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "حذف الرابط المشترك",
|
"delete_shared_link_dialog_title": "حذف الرابط المشترك",
|
||||||
"description_input_hint_text": "اضف وصفا...",
|
"description_input_hint_text": "اضف وصفا...",
|
||||||
"description_input_submit_error": "خطأ تحديث الوصف ، تحقق من السجل لمزيد من التفاصيل",
|
"description_input_submit_error": "خطأ تحديث الوصف ، تحقق من السجل لمزيد من التفاصيل",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "التاريخ و الوقت",
|
"edit_date_time_dialog_date_time": "التاريخ و الوقت",
|
||||||
"edit_date_time_dialog_timezone": "وحدة زمنية",
|
"edit_date_time_dialog_timezone": "وحدة زمنية",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
"assets_trashed_from_server": "{} položek vyhozeno do koše na Immich serveru",
|
"assets_trashed_from_server": "{} položek vyhozeno do koše na Immich serveru",
|
||||||
"asset_viewer_settings_title": "Prohlížeč",
|
"asset_viewer_settings_title": "Prohlížeč",
|
||||||
"backup_album_selection_page_albums_device": "Alba v zařízení ({})",
|
"backup_album_selection_page_albums_device": "Alba v zařízení ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, dvojím klepnutím ji vyloučíte",
|
"backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, opětovným klepnutím ji vyloučíte",
|
||||||
"backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.",
|
"backup_album_selection_page_assets_scatter": "Položky mohou být roztroušeny ve více albech. To umožňuje zahrnout nebo vyloučit alba během procesu zálohování.",
|
||||||
"backup_album_selection_page_select_albums": "Vybraná alba",
|
"backup_album_selection_page_select_albums": "Vybraná alba",
|
||||||
"backup_album_selection_page_selection_info": "Informace o výběru",
|
"backup_album_selection_page_selection_info": "Informace o výběru",
|
||||||
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Odstranit sdílený odkaz",
|
"delete_shared_link_dialog_title": "Odstranit sdílený odkaz",
|
||||||
"description_input_hint_text": "Přidat popis...",
|
"description_input_hint_text": "Přidat popis...",
|
||||||
"description_input_submit_error": "Chyba aktualizace popisu, další podrobnosti najdete v logu",
|
"description_input_submit_error": "Chyba aktualizace popisu, další podrobnosti najdete v logu",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Datum a čas",
|
"edit_date_time_dialog_date_time": "Datum a čas",
|
||||||
"edit_date_time_dialog_timezone": "Časové pásmo",
|
"edit_date_time_dialog_timezone": "Časové pásmo",
|
||||||
"edit_image_title": "Upravit",
|
"edit_image_title": "Upravit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Slet delt link",
|
"delete_shared_link_dialog_title": "Slet delt link",
|
||||||
"description_input_hint_text": "Tilføj en beskrivelse...",
|
"description_input_hint_text": "Tilføj en beskrivelse...",
|
||||||
"description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer",
|
"description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Dato og klokkeslæt",
|
"edit_date_time_dialog_date_time": "Dato og klokkeslæt",
|
||||||
"edit_date_time_dialog_timezone": "Tidszone",
|
"edit_date_time_dialog_timezone": "Tidszone",
|
||||||
"edit_image_title": "Rediger",
|
"edit_image_title": "Rediger",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Geteilten Link löschen",
|
"delete_shared_link_dialog_title": "Geteilten Link löschen",
|
||||||
"description_input_hint_text": "Beschreibung hinzufügen...",
|
"description_input_hint_text": "Beschreibung hinzufügen...",
|
||||||
"description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.",
|
"description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Datum und Uhrzeit",
|
"edit_date_time_dialog_date_time": "Datum und Uhrzeit",
|
||||||
"edit_date_time_dialog_timezone": "Zeitzone",
|
"edit_date_time_dialog_timezone": "Zeitzone",
|
||||||
"edit_image_title": "Bearbeiten",
|
"edit_image_title": "Bearbeiten",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Διαγραφή Κοινοποιημένου Συνδέσμου",
|
"delete_shared_link_dialog_title": "Διαγραφή Κοινοποιημένου Συνδέσμου",
|
||||||
"description_input_hint_text": "Προσθήκη περιγραφής...",
|
"description_input_hint_text": "Προσθήκη περιγραφής...",
|
||||||
"description_input_submit_error": "Σφάλμα κατά την ενημέρωση της περιγραφής, ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες",
|
"description_input_submit_error": "Σφάλμα κατά την ενημέρωση της περιγραφής, ελέγξτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Ημερομηνία και Ώρα",
|
"edit_date_time_dialog_date_time": "Ημερομηνία και Ώρα",
|
||||||
"edit_date_time_dialog_timezone": "Ζώνη ώρας",
|
"edit_date_time_dialog_timezone": "Ζώνη ώρας",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
@ -252,10 +256,9 @@
|
|||||||
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
||||||
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
||||||
"image_saved_successfully": "Image saved",
|
"image_saved_successfully": "Image saved",
|
||||||
"download_error": "Download Error",
|
"image_viewer_page_state_provider_download_error": "Download Error",
|
||||||
"download_started": "Download started",
|
"image_viewer_page_state_provider_download_started": "Download Started",
|
||||||
"download_sucess": "Download success",
|
"image_viewer_page_state_provider_download_success": "Download Success",
|
||||||
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
|
||||||
"image_viewer_page_state_provider_share_error": "Share Error",
|
"image_viewer_page_state_provider_share_error": "Share Error",
|
||||||
"invalid_date": "Invalid date",
|
"invalid_date": "Invalid date",
|
||||||
"invalid_date_format": "Invalid date format",
|
"invalid_date_format": "Invalid date format",
|
||||||
|
@ -173,8 +173,8 @@
|
|||||||
"control_bottom_app_bar_delete": "Eliminar",
|
"control_bottom_app_bar_delete": "Eliminar",
|
||||||
"control_bottom_app_bar_delete_from_immich": "Borrar de Immich",
|
"control_bottom_app_bar_delete_from_immich": "Borrar de Immich",
|
||||||
"control_bottom_app_bar_delete_from_local": "Borrar del dispositivo",
|
"control_bottom_app_bar_delete_from_local": "Borrar del dispositivo",
|
||||||
"control_bottom_app_bar_download": "Download",
|
"control_bottom_app_bar_download": "Descargar",
|
||||||
"control_bottom_app_bar_edit": "Edit",
|
"control_bottom_app_bar_edit": "Editar",
|
||||||
"control_bottom_app_bar_edit_location": "Editar ubicación",
|
"control_bottom_app_bar_edit_location": "Editar ubicación",
|
||||||
"control_bottom_app_bar_edit_time": "Editar fecha y hora",
|
"control_bottom_app_bar_edit_time": "Editar fecha y hora",
|
||||||
"control_bottom_app_bar_favorite": "Favorito",
|
"control_bottom_app_bar_favorite": "Favorito",
|
||||||
@ -190,7 +190,7 @@
|
|||||||
"create_shared_album_page_share": "Compartir",
|
"create_shared_album_page_share": "Compartir",
|
||||||
"create_shared_album_page_share_add_assets": "AGREGAR ELEMENTOS",
|
"create_shared_album_page_share_add_assets": "AGREGAR ELEMENTOS",
|
||||||
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
|
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
|
||||||
"crop": "Crop",
|
"crop": "Recortar",
|
||||||
"curated_location_page_title": "Lugares",
|
"curated_location_page_title": "Lugares",
|
||||||
"curated_object_page_title": "Objetos",
|
"curated_object_page_title": "Objetos",
|
||||||
"daily_title_text_date": "E dd, MMM",
|
"daily_title_text_date": "E dd, MMM",
|
||||||
@ -210,9 +210,13 @@
|
|||||||
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
||||||
"description_input_hint_text": "Agregar descripción...",
|
"description_input_hint_text": "Agregar descripción...",
|
||||||
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Fecha y Hora",
|
"edit_date_time_dialog_date_time": "Fecha y Hora",
|
||||||
"edit_date_time_dialog_timezone": "Zona horaria",
|
"edit_date_time_dialog_timezone": "Zona horaria",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Editar",
|
||||||
"edit_location_dialog_title": "Ubicación",
|
"edit_location_dialog_title": "Ubicación",
|
||||||
"error_saving_image": "Error: {}",
|
"error_saving_image": "Error: {}",
|
||||||
"exif_bottom_sheet_description": "Agregar Descripción...",
|
"exif_bottom_sheet_description": "Agregar Descripción...",
|
||||||
@ -251,13 +255,13 @@
|
|||||||
"home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
|
"home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
|
||||||
"home_page_share_err_local": "No se pueden compartir elementos locales a través de un enlace, omitiendo",
|
"home_page_share_err_local": "No se pueden compartir elementos locales a través de un enlace, omitiendo",
|
||||||
"home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo",
|
"home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo",
|
||||||
"image_saved_successfully": "Image saved",
|
"image_saved_successfully": "Imágenes guardas",
|
||||||
"image_viewer_page_state_provider_download_error": "Error de descarga",
|
"image_viewer_page_state_provider_download_error": "Error de descarga",
|
||||||
"image_viewer_page_state_provider_download_started": "Descarga Iniciada",
|
"image_viewer_page_state_provider_download_started": "Descarga Iniciada",
|
||||||
"image_viewer_page_state_provider_download_success": "Descarga exitosa",
|
"image_viewer_page_state_provider_download_success": "Descarga exitosa",
|
||||||
"image_viewer_page_state_provider_share_error": "Error al compartir",
|
"image_viewer_page_state_provider_share_error": "Error al compartir",
|
||||||
"invalid_date": "Invalid date",
|
"invalid_date": "Fecha incorrecta",
|
||||||
"invalid_date_format": "Invalid date format",
|
"invalid_date_format": "Formato de fecha incorrecto",
|
||||||
"library_page_albums": "Álbumes",
|
"library_page_albums": "Álbumes",
|
||||||
"library_page_archive": "Archivo",
|
"library_page_archive": "Archivo",
|
||||||
"library_page_device_albums": "Álbumes en el dispositivo",
|
"library_page_device_albums": "Álbumes en el dispositivo",
|
||||||
@ -380,27 +384,27 @@
|
|||||||
"profile_drawer_sign_out": "Cerrar Sesión",
|
"profile_drawer_sign_out": "Cerrar Sesión",
|
||||||
"profile_drawer_trash": "Papelera",
|
"profile_drawer_trash": "Papelera",
|
||||||
"recently_added_page_title": "Recién Agregadas",
|
"recently_added_page_title": "Recién Agregadas",
|
||||||
"save_to_gallery": "Save to gallery",
|
"save_to_gallery": "Guardado en la galería",
|
||||||
"scaffold_body_error_occurred": "Ha ocurrido un error",
|
"scaffold_body_error_occurred": "Ha ocurrido un error",
|
||||||
"search_bar_hint": "Busca tus fotos",
|
"search_bar_hint": "Busca tus fotos",
|
||||||
"search_filter_apply": "Aplicar filtros",
|
"search_filter_apply": "Aplicar filtros",
|
||||||
"search_filter_camera": "Camera",
|
"search_filter_camera": "Cámara",
|
||||||
"search_filter_camera_make": "Marca",
|
"search_filter_camera_make": "Marca",
|
||||||
"search_filter_camera_model": "Modelo",
|
"search_filter_camera_model": "Modelo",
|
||||||
"search_filter_camera_title": "Select camera type",
|
"search_filter_camera_title": "Select camera type",
|
||||||
"search_filter_date": "Date",
|
"search_filter_date": "Fecha",
|
||||||
"search_filter_date_interval": "{start} to {end}",
|
"search_filter_date_interval": "{start} al {end}",
|
||||||
"search_filter_date_title": "Select a date range",
|
"search_filter_date_title": "Selecciona un intervalo de fechas",
|
||||||
"search_filter_display_option_archive": "Archivado",
|
"search_filter_display_option_archive": "Archivado",
|
||||||
"search_filter_display_option_favorite": "Favorito",
|
"search_filter_display_option_favorite": "Favorito",
|
||||||
"search_filter_display_option_not_in_album": "No en álbum",
|
"search_filter_display_option_not_in_album": "No en álbum",
|
||||||
"search_filter_display_options": "Display Options",
|
"search_filter_display_options": "Display Options",
|
||||||
"search_filter_display_options_title": "Display options",
|
"search_filter_display_options_title": "Display options",
|
||||||
"search_filter_location": "Location",
|
"search_filter_location": "Ubicación",
|
||||||
"search_filter_location_city": "Ciudad",
|
"search_filter_location_city": "Ciudad",
|
||||||
"search_filter_location_country": "País",
|
"search_filter_location_country": "País",
|
||||||
"search_filter_location_state": "Estado",
|
"search_filter_location_state": "Estado",
|
||||||
"search_filter_location_title": "Select location",
|
"search_filter_location_title": "Seleccionar una ubicación",
|
||||||
"search_filter_media_type": "Media Type",
|
"search_filter_media_type": "Media Type",
|
||||||
"search_filter_media_type_all": "Todos",
|
"search_filter_media_type_all": "Todos",
|
||||||
"search_filter_media_type_image": "Imagen",
|
"search_filter_media_type_image": "Imagen",
|
||||||
@ -535,7 +539,7 @@
|
|||||||
"sharing_silver_appbar_create_shared_album": "Crear un álbum compartido",
|
"sharing_silver_appbar_create_shared_album": "Crear un álbum compartido",
|
||||||
"sharing_silver_appbar_shared_links": "Enlaces compartidos",
|
"sharing_silver_appbar_shared_links": "Enlaces compartidos",
|
||||||
"sharing_silver_appbar_share_partner": "Compartir con el compañero",
|
"sharing_silver_appbar_share_partner": "Compartir con el compañero",
|
||||||
"sync": "Sync",
|
"sync": "Sincronizar",
|
||||||
"sync_albums": "Sync albums",
|
"sync_albums": "Sync albums",
|
||||||
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
||||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
||||||
"description_input_hint_text": "Agregar descripción...",
|
"description_input_hint_text": "Agregar descripción...",
|
||||||
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
||||||
"description_input_hint_text": "Agregar descripción...",
|
"description_input_hint_text": "Agregar descripción...",
|
||||||
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
|
||||||
"description_input_hint_text": "Agregar descripción...",
|
"description_input_hint_text": "Agregar descripción...",
|
||||||
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Poista jaettu linkki",
|
"delete_shared_link_dialog_title": "Poista jaettu linkki",
|
||||||
"description_input_hint_text": "Lisää kuvaus...",
|
"description_input_hint_text": "Lisää kuvaus...",
|
||||||
"description_input_submit_error": "Virhe kuvauksen päivittämisessä, tarkista lisätiedot lokista",
|
"description_input_submit_error": "Virhe kuvauksen päivittämisessä, tarkista lisätiedot lokista",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Päivämäärä ja aika",
|
"edit_date_time_dialog_date_time": "Päivämäärä ja aika",
|
||||||
"edit_date_time_dialog_timezone": "Aikavyöhyke",
|
"edit_date_time_dialog_timezone": "Aikavyöhyke",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Supprimer le lien partagé",
|
"delete_shared_link_dialog_title": "Supprimer le lien partagé",
|
||||||
"description_input_hint_text": "Ajouter une description...",
|
"description_input_hint_text": "Ajouter une description...",
|
||||||
"description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
|
"description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Supprimer le lien partagé",
|
"delete_shared_link_dialog_title": "Supprimer le lien partagé",
|
||||||
"description_input_hint_text": "Ajouter une description…",
|
"description_input_hint_text": "Ajouter une description…",
|
||||||
"description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
|
"description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date et heure",
|
"edit_date_time_dialog_date_time": "Date et heure",
|
||||||
"edit_date_time_dialog_timezone": "Fuseau horaire",
|
"edit_date_time_dialog_timezone": "Fuseau horaire",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -190,7 +190,7 @@
|
|||||||
"create_shared_album_page_share": "שתף",
|
"create_shared_album_page_share": "שתף",
|
||||||
"create_shared_album_page_share_add_assets": "הוסף נכסים",
|
"create_shared_album_page_share_add_assets": "הוסף נכסים",
|
||||||
"create_shared_album_page_share_select_photos": "בחירת תמונות",
|
"create_shared_album_page_share_select_photos": "בחירת תמונות",
|
||||||
"crop": "Crop",
|
"crop": "חתוך",
|
||||||
"curated_location_page_title": "מקומות",
|
"curated_location_page_title": "מקומות",
|
||||||
"curated_object_page_title": "דברים",
|
"curated_object_page_title": "דברים",
|
||||||
"daily_title_text_date": "E, MMM dd",
|
"daily_title_text_date": "E, MMM dd",
|
||||||
@ -210,11 +210,15 @@
|
|||||||
"delete_shared_link_dialog_title": "מחק קישור משותף",
|
"delete_shared_link_dialog_title": "מחק קישור משותף",
|
||||||
"description_input_hint_text": "הוסף תיאור...",
|
"description_input_hint_text": "הוסף תיאור...",
|
||||||
"description_input_submit_error": "שגיאה בעדכון תיאור, בדוק את היומן לפרטים נוספים",
|
"description_input_submit_error": "שגיאה בעדכון תיאור, בדוק את היומן לפרטים נוספים",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "תאריך וזמן",
|
"edit_date_time_dialog_date_time": "תאריך וזמן",
|
||||||
"edit_date_time_dialog_timezone": "אזור זמן",
|
"edit_date_time_dialog_timezone": "אזור זמן",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "ערוך",
|
||||||
"edit_location_dialog_title": "מיקום",
|
"edit_location_dialog_title": "מיקום",
|
||||||
"error_saving_image": "Error: {}",
|
"error_saving_image": "שגיאה: {}",
|
||||||
"exif_bottom_sheet_description": "הוסף תיאור...",
|
"exif_bottom_sheet_description": "הוסף תיאור...",
|
||||||
"exif_bottom_sheet_details": "פרטים",
|
"exif_bottom_sheet_details": "פרטים",
|
||||||
"exif_bottom_sheet_location": "מיקום",
|
"exif_bottom_sheet_location": "מיקום",
|
||||||
@ -251,7 +255,7 @@
|
|||||||
"home_page_first_time_notice": "אם זאת הפעם הראשונה שאת/ה משתמש/ת ביישום, נא להקפיד לבחור אלבומ(ים) לגיבוי כך שציר הזמן יוכל לאכלס תמונות וסרטונים באלבומ(ים)",
|
"home_page_first_time_notice": "אם זאת הפעם הראשונה שאת/ה משתמש/ת ביישום, נא להקפיד לבחור אלבומ(ים) לגיבוי כך שציר הזמן יוכל לאכלס תמונות וסרטונים באלבומ(ים)",
|
||||||
"home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג",
|
"home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג",
|
||||||
"home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 נכסים בכל פעם, מדלג",
|
"home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 נכסים בכל פעם, מדלג",
|
||||||
"image_saved_successfully": "Image saved",
|
"image_saved_successfully": "תמונה נשמרה",
|
||||||
"image_viewer_page_state_provider_download_error": "שגיאת הורדה",
|
"image_viewer_page_state_provider_download_error": "שגיאת הורדה",
|
||||||
"image_viewer_page_state_provider_download_started": "ההורדה החלה",
|
"image_viewer_page_state_provider_download_started": "ההורדה החלה",
|
||||||
"image_viewer_page_state_provider_download_success": "הצלחת הורדה",
|
"image_viewer_page_state_provider_download_success": "הצלחת הורדה",
|
||||||
@ -380,7 +384,7 @@
|
|||||||
"profile_drawer_sign_out": "יציאה",
|
"profile_drawer_sign_out": "יציאה",
|
||||||
"profile_drawer_trash": "אשפה",
|
"profile_drawer_trash": "אשפה",
|
||||||
"recently_added_page_title": "נוסף לאחרונה",
|
"recently_added_page_title": "נוסף לאחרונה",
|
||||||
"save_to_gallery": "Save to gallery",
|
"save_to_gallery": "שמור לגלריה",
|
||||||
"scaffold_body_error_occurred": "אירעה שגיאה",
|
"scaffold_body_error_occurred": "אירעה שגיאה",
|
||||||
"search_bar_hint": "חפש/י בתמונות שלך",
|
"search_bar_hint": "חפש/י בתמונות שלך",
|
||||||
"search_filter_apply": "החל סינון",
|
"search_filter_apply": "החל סינון",
|
||||||
@ -535,10 +539,10 @@
|
|||||||
"sharing_silver_appbar_create_shared_album": "אלבום משותף חדש",
|
"sharing_silver_appbar_create_shared_album": "אלבום משותף חדש",
|
||||||
"sharing_silver_appbar_shared_links": "קישורים משותפים",
|
"sharing_silver_appbar_shared_links": "קישורים משותפים",
|
||||||
"sharing_silver_appbar_share_partner": "שיתוף עם שותף",
|
"sharing_silver_appbar_share_partner": "שיתוף עם שותף",
|
||||||
"sync": "Sync",
|
"sync": "סנכרן",
|
||||||
"sync_albums": "Sync albums",
|
"sync_albums": "סנכרן אלבומים",
|
||||||
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
"sync_albums_manual_subtitle": "סנכרן את כל הסרטונים והתמונות שהועלו לאלבומי הגיבוי שנבחרו",
|
||||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
"sync_upload_album_setting_subtitle": "צור והעלה תמונות וסרטונים שלך לאלבומים שנבחרו ביישום",
|
||||||
"tab_controller_nav_library": "ספרייה",
|
"tab_controller_nav_library": "ספרייה",
|
||||||
"tab_controller_nav_photos": "תמונות",
|
"tab_controller_nav_photos": "תמונות",
|
||||||
"tab_controller_nav_search": "חיפוש",
|
"tab_controller_nav_search": "חיפוש",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "साझा किए गए लिंक को हटाएं",
|
"delete_shared_link_dialog_title": "साझा किए गए लिंक को हटाएं",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Megosztott Link Törlése",
|
"delete_shared_link_dialog_title": "Megosztott Link Törlése",
|
||||||
"description_input_hint_text": "Leírás hozzáadása...",
|
"description_input_hint_text": "Leírás hozzáadása...",
|
||||||
"description_input_submit_error": "Nem sikerült frissíteni a leírást. További információért kérjük, nézd meg az eseménynaplót",
|
"description_input_submit_error": "Nem sikerült frissíteni a leírást. További információért kérjük, nézd meg az eseménynaplót",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Dátum és Idő",
|
"edit_date_time_dialog_date_time": "Dátum és Idő",
|
||||||
"edit_date_time_dialog_timezone": "Időzóna",
|
"edit_date_time_dialog_timezone": "Időzóna",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Elimina link condiviso",
|
"delete_shared_link_dialog_title": "Elimina link condiviso",
|
||||||
"description_input_hint_text": "Aggiungi descrizione...",
|
"description_input_hint_text": "Aggiungi descrizione...",
|
||||||
"description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli",
|
"description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Data e ora",
|
"edit_date_time_dialog_date_time": "Data e ora",
|
||||||
"edit_date_time_dialog_timezone": "Fuso orario",
|
"edit_date_time_dialog_timezone": "Fuso orario",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "共有リンクを消す",
|
"delete_shared_link_dialog_title": "共有リンクを消す",
|
||||||
"description_input_hint_text": "説明を追加",
|
"description_input_hint_text": "説明を追加",
|
||||||
"description_input_submit_error": "説明の編集に失敗しました。詳細はログを確認してください。",
|
"description_input_submit_error": "説明の編集に失敗しました。詳細はログを確認してください。",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "日付と時間",
|
"edit_date_time_dialog_date_time": "日付と時間",
|
||||||
"edit_date_time_dialog_timezone": "タイムゾーン",
|
"edit_date_time_dialog_timezone": "タイムゾーン",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
"album_viewer_appbar_share_err_delete": "앨범을 삭제하지 못했습니다.",
|
"album_viewer_appbar_share_err_delete": "앨범을 삭제하지 못했습니다.",
|
||||||
"album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다.",
|
"album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다.",
|
||||||
"album_viewer_appbar_share_err_remove": "앨범에서 항목을 제거하지 못했습니다.",
|
"album_viewer_appbar_share_err_remove": "앨범에서 항목을 제거하지 못했습니다.",
|
||||||
"album_viewer_appbar_share_err_title": "앨범 이름을 변경하지 못했습니다.",
|
"album_viewer_appbar_share_err_title": "앨범명을 변경하지 못했습니다.",
|
||||||
"album_viewer_appbar_share_leave": "앨범 나가기",
|
"album_viewer_appbar_share_leave": "앨범 나가기",
|
||||||
"album_viewer_appbar_share_remove": "앨범에서 제거",
|
"album_viewer_appbar_share_remove": "앨범에서 제거",
|
||||||
"album_viewer_appbar_share_to": "공유 대상",
|
"album_viewer_appbar_share_to": "공유 대상",
|
||||||
@ -55,12 +55,12 @@
|
|||||||
"asset_list_settings_subtitle": "사진 배열 레이아웃 설정",
|
"asset_list_settings_subtitle": "사진 배열 레이아웃 설정",
|
||||||
"asset_list_settings_title": "사진 배열",
|
"asset_list_settings_title": "사진 배열",
|
||||||
"asset_restored_successfully": "항목이 성공적으로 복원되었습니다.",
|
"asset_restored_successfully": "항목이 성공적으로 복원되었습니다.",
|
||||||
"assets_deleted_permanently": "{} 미디어가 영구 삭제됨",
|
"assets_deleted_permanently": "{}개 항목이 영구적으로 삭제됨",
|
||||||
"assets_deleted_permanently_from_server": "Immich 서버에서 {} 미디어가 영구 삭제되었습니다.",
|
"assets_deleted_permanently_from_server": "{}개 항목이 Immich 서버에서 영구적으로 삭제됨",
|
||||||
"assets_removed_permanently_from_device": "장치에서 {} 미디어가 영구적으로 제거되었습니다.",
|
"assets_removed_permanently_from_device": "{}개 항목이 기기에서 영구적으로 삭제됨",
|
||||||
"assets_restored_successfully": "{} 미디어가 성공적으로 복원되었습니다.",
|
"assets_restored_successfully": "항목 {}개를 복원했습니다.",
|
||||||
"assets_trashed": "{} 미디어가 휴지통에 버려졌습니다.",
|
"assets_trashed": "휴지통으로 {}개 항목이 이동되었습니다.",
|
||||||
"assets_trashed_from_server": "Immich 서버에서 {} 미디어를 삭제했습니다.",
|
"assets_trashed_from_server": "휴지통으로 Immich 서버의 {}개 항목이 이동되었습니다.",
|
||||||
"asset_viewer_settings_title": "보기 옵션",
|
"asset_viewer_settings_title": "보기 옵션",
|
||||||
"backup_album_selection_page_albums_device": "기기의 앨범 ({})",
|
"backup_album_selection_page_albums_device": "기기의 앨범 ({})",
|
||||||
"backup_album_selection_page_albums_tap": "한 번 눌러 선택, 두 번 눌러 제외하세요.",
|
"backup_album_selection_page_albums_tap": "한 번 눌러 선택, 두 번 눌러 제외하세요.",
|
||||||
@ -69,55 +69,55 @@
|
|||||||
"backup_album_selection_page_selection_info": "선택한 앨범",
|
"backup_album_selection_page_selection_info": "선택한 앨범",
|
||||||
"backup_album_selection_page_total_assets": "전체 항목",
|
"backup_album_selection_page_total_assets": "전체 항목",
|
||||||
"backup_all": "모두",
|
"backup_all": "모두",
|
||||||
"backup_background_service_backup_failed_message": "백업하지 못했습니다. 다시 시도하는 중...",
|
"backup_background_service_backup_failed_message": "항목을 백업하지 못했습니다. 다시 시도하는 중...",
|
||||||
"backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...",
|
"backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...",
|
||||||
"backup_background_service_current_upload_notification": "{} 업로드 중",
|
"backup_background_service_current_upload_notification": "{} 업로드 중",
|
||||||
"backup_background_service_default_notification": "백업할 항목을 확인하는 중...",
|
"backup_background_service_default_notification": "백업할 항목을 확인하는 중...",
|
||||||
"backup_background_service_error_title": "백업 오류",
|
"backup_background_service_error_title": "백업 오류",
|
||||||
"backup_background_service_in_progress_notification": "선택한 항목을 백업하는 중...",
|
"backup_background_service_in_progress_notification": "선택한 항목을 백업하는 중...",
|
||||||
"backup_background_service_upload_failure_notification": "{} 업로드 실패",
|
"backup_background_service_upload_failure_notification": "{} 업로드 실패",
|
||||||
"backup_controller_page_albums": "백업 대상 앨범",
|
"backup_controller_page_albums": "백업할 앨범",
|
||||||
"backup_controller_page_background_app_refresh_disabled_content": "백그라운드 백업을 사용하려면 설정 > 일반 > 백그라운드 앱 새로 고침에서 백그라운드 앱 새로 고침을 활성화하세요.",
|
"backup_controller_page_background_app_refresh_disabled_content": "백그라운드 백업을 사용하려면 설정 > 일반 > 백그라운드 앱 새로 고침에서 백그라운드 앱 새로 고침을 활성화하세요.",
|
||||||
"backup_controller_page_background_app_refresh_disabled_title": "백그라운드 새로 고침 비활성화됨",
|
"backup_controller_page_background_app_refresh_disabled_title": "백그라운드 새로 고침 비활성화됨",
|
||||||
"backup_controller_page_background_app_refresh_enable_button_text": "설정으로 이동",
|
"backup_controller_page_background_app_refresh_enable_button_text": "설정으로 이동",
|
||||||
"backup_controller_page_background_battery_info_link": "설정 방법",
|
"backup_controller_page_background_battery_info_link": "설정 방법",
|
||||||
"backup_controller_page_background_battery_info_message": "최상의 백그라운드 백업 환경을 위해, Immich의 백그라운드 활동을 제한하는 배터리 최적화를 비활성화하세요.\n\n설정 방법은 기기마다 다르므로, 제조 업체에서 관련 정보를 찾아보세요.",
|
"backup_controller_page_background_battery_info_message": "최상의 백그라운드 백업 환경을 위해 Immich 백그라운드 활동을 제한하는 배터리 최적화 기능을 비활성화하세요.\n\n기기마다 설정 방법에 차이가 있어 제조 업체에서 관련 정보를 찾아보세요.",
|
||||||
"backup_controller_page_background_battery_info_ok": "확인",
|
"backup_controller_page_background_battery_info_ok": "확인",
|
||||||
"backup_controller_page_background_battery_info_title": "배터리 최적화",
|
"backup_controller_page_background_battery_info_title": "배터리 최적화",
|
||||||
"backup_controller_page_background_charging": "충전 중에만",
|
"backup_controller_page_background_charging": "충전 중에만",
|
||||||
"backup_controller_page_background_configure_error": "백그라운드 서비스 구성 실패",
|
"backup_controller_page_background_configure_error": "백그라운드 서비스 구성 실패",
|
||||||
"backup_controller_page_background_delay": "새 콘텐츠 백업 간격: {}",
|
"backup_controller_page_background_delay": "새 콘텐츠 백업 간격: {}",
|
||||||
"backup_controller_page_background_description": "백그라운드 서비스를 활성화하면 앱을 열지 않고도 새 콘텐츠를 자동으로 백업할 수 있습니다.",
|
"backup_controller_page_background_description": "백그라운드 서비스를 활성화하여 앱을 실행하지 않고 새 항목을 자동으로 백업하세요.",
|
||||||
"backup_controller_page_background_is_off": "자동 백그라운드 백업이 비활성화되었습니다.",
|
"backup_controller_page_background_is_off": "백그라운드 백업이 비활성화되었습니다.",
|
||||||
"backup_controller_page_background_is_on": "자동 백그라운드 백업이 활성화되었습니다.",
|
"backup_controller_page_background_is_on": "백그라운드 백업이 활성화되었습니다.",
|
||||||
"backup_controller_page_background_turn_off": "백그라운드 서비스 비활성화",
|
"backup_controller_page_background_turn_off": "백그라운드 서비스 비활성화",
|
||||||
"backup_controller_page_background_turn_on": "백그라운드 서비스 활성화",
|
"backup_controller_page_background_turn_on": "백그라운드 서비스 활성화",
|
||||||
"backup_controller_page_background_wifi": "Wi-Fi에서만",
|
"backup_controller_page_background_wifi": "Wi-Fi에서만",
|
||||||
"backup_controller_page_backup": "백업",
|
"backup_controller_page_backup": "백업",
|
||||||
"backup_controller_page_backup_selected": "선택: ",
|
"backup_controller_page_backup_selected": "선택됨:",
|
||||||
"backup_controller_page_backup_sub": "백업된 사진 및 동영상",
|
"backup_controller_page_backup_sub": "백업된 사진 및 동영상",
|
||||||
"backup_controller_page_cancel": "취소",
|
"backup_controller_page_cancel": "취소",
|
||||||
"backup_controller_page_created": "생성일: {}",
|
"backup_controller_page_created": "생성일: {}",
|
||||||
"backup_controller_page_desc_backup": "앱을 열 때 새 항목을 서버에 자동으로 업로드하려면 포그라운드 백업을 활성화하세요.",
|
"backup_controller_page_desc_backup": "포그라운드 백업을 활성화하여 앱을 시작할 때 새 항목을 서버에 자동으로 업로드하세요.",
|
||||||
"backup_controller_page_excluded": "제외: ",
|
"backup_controller_page_excluded": "제외됨:",
|
||||||
"backup_controller_page_failed": "실패 ({})",
|
"backup_controller_page_failed": "실패 ({})",
|
||||||
"backup_controller_page_filename": "파일명: {} [{}]",
|
"backup_controller_page_filename": "파일명: {} [{}]",
|
||||||
"backup_controller_page_id": "ID: {}",
|
"backup_controller_page_id": "ID: {}",
|
||||||
"backup_controller_page_info": "백업 정보",
|
"backup_controller_page_info": "백업 정보",
|
||||||
"backup_controller_page_none_selected": "선택한 항목이 없습니다.",
|
"backup_controller_page_none_selected": "선택한 항목이 없습니다.",
|
||||||
"backup_controller_page_remainder": "남은 항목",
|
"backup_controller_page_remainder": "남은 항목",
|
||||||
"backup_controller_page_remainder_sub": "백업할 사진 및 동영상",
|
"backup_controller_page_remainder_sub": "백업 대기 중인 사진 및 동영상",
|
||||||
"backup_controller_page_select": "선택",
|
"backup_controller_page_select": "선택",
|
||||||
"backup_controller_page_server_storage": "저장 공간",
|
"backup_controller_page_server_storage": "저장 공간",
|
||||||
"backup_controller_page_start_backup": "백업 시작",
|
"backup_controller_page_start_backup": "백업 시작",
|
||||||
"backup_controller_page_status_off": "자동 백업이 비활성화되었습니다.",
|
"backup_controller_page_status_off": "포그라운드 백업이 비활성화되었습니다.",
|
||||||
"backup_controller_page_status_on": "자동 백업이 활성화되었습니다.",
|
"backup_controller_page_status_on": "포그라운드 백업이 활성화되었습니다.",
|
||||||
"backup_controller_page_storage_format": "{} 사용 중, 전체 {}",
|
"backup_controller_page_storage_format": "{} 사용 중, 전체 {}",
|
||||||
"backup_controller_page_to_backup": "백업 대상 앨범 목록",
|
"backup_controller_page_to_backup": "백업할 앨범 목록",
|
||||||
"backup_controller_page_total": "전체",
|
"backup_controller_page_total": "전체",
|
||||||
"backup_controller_page_total_sub": "선택한 앨범의 모든 사진 및 동영상",
|
"backup_controller_page_total_sub": "선택한 앨범의 고유한 사진 및 동영상",
|
||||||
"backup_controller_page_turn_off": "백업 비활성화",
|
"backup_controller_page_turn_off": "비활성화",
|
||||||
"backup_controller_page_turn_on": "백업 활성화",
|
"backup_controller_page_turn_on": "활성화",
|
||||||
"backup_controller_page_uploading_file_info": "파일 정보 업로드 중",
|
"backup_controller_page_uploading_file_info": "파일 정보 업로드 중",
|
||||||
"backup_err_only_album": "유일한 앨범은 제거할 수 없습니다.",
|
"backup_err_only_album": "유일한 앨범은 제거할 수 없습니다.",
|
||||||
"backup_info_card_assets": "항목",
|
"backup_info_card_assets": "항목",
|
||||||
@ -154,7 +154,7 @@
|
|||||||
"client_cert_enter_password": "비밀번호 입력",
|
"client_cert_enter_password": "비밀번호 입력",
|
||||||
"client_cert_import": "가져오기",
|
"client_cert_import": "가져오기",
|
||||||
"client_cert_import_success_msg": "클라이언트 인증서를 가져왔습니다.",
|
"client_cert_import_success_msg": "클라이언트 인증서를 가져왔습니다.",
|
||||||
"client_cert_invalid_msg": "유효하지 않은 인증서이거나 비밀번호가 일치하지 않습니다.",
|
"client_cert_invalid_msg": "유효하지 않은 인증서 또는 패스프레이즈가 일치하지 않습니다.",
|
||||||
"client_cert_remove": "제거",
|
"client_cert_remove": "제거",
|
||||||
"client_cert_remove_msg": "클라이언트 인증서가 제거되었습니다.",
|
"client_cert_remove_msg": "클라이언트 인증서가 제거되었습니다.",
|
||||||
"client_cert_subtitle": "인증서 가져오기/제거는 로그인 전에만 가능합니다. PKCS12 (.p12, .pfx) 형식을 지원합니다.",
|
"client_cert_subtitle": "인증서 가져오기/제거는 로그인 전에만 가능합니다. PKCS12 (.p12, .pfx) 형식을 지원합니다.",
|
||||||
@ -210,11 +210,15 @@
|
|||||||
"delete_shared_link_dialog_title": "공유 링크 삭제",
|
"delete_shared_link_dialog_title": "공유 링크 삭제",
|
||||||
"description_input_hint_text": "설명 추가...",
|
"description_input_hint_text": "설명 추가...",
|
||||||
"description_input_submit_error": "설명을 변경하는 중 문제가 발생했습니다. 자세한 내용은 로그를 참조하세요.",
|
"description_input_submit_error": "설명을 변경하는 중 문제가 발생했습니다. 자세한 내용은 로그를 참조하세요.",
|
||||||
|
"download_error": "다운로드 중 문제가 발생했습니다.",
|
||||||
|
"download_started": "다운로드가 시작되었습니다.",
|
||||||
|
"download_sucess": "다운로드가 완료되었습니다.",
|
||||||
|
"download_sucess_android": "미디어가 DCIM/Immich에 저장되었습니다.",
|
||||||
"edit_date_time_dialog_date_time": "날짜 및 시간",
|
"edit_date_time_dialog_date_time": "날짜 및 시간",
|
||||||
"edit_date_time_dialog_timezone": "시간대",
|
"edit_date_time_dialog_timezone": "시간대",
|
||||||
"edit_image_title": "편집",
|
"edit_image_title": "편집",
|
||||||
"edit_location_dialog_title": "위치",
|
"edit_location_dialog_title": "위치",
|
||||||
"error_saving_image": "오류입니다: {}",
|
"error_saving_image": "오류: {}",
|
||||||
"exif_bottom_sheet_description": "설명 추가...",
|
"exif_bottom_sheet_description": "설명 추가...",
|
||||||
"exif_bottom_sheet_details": "상세 정보",
|
"exif_bottom_sheet_details": "상세 정보",
|
||||||
"exif_bottom_sheet_location": "위치",
|
"exif_bottom_sheet_location": "위치",
|
||||||
@ -251,7 +255,7 @@
|
|||||||
"home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인에 앨범의 사진과 동영상을 채울 수 있도록 백업할 앨범을 선택하세요.",
|
"home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인에 앨범의 사진과 동영상을 채울 수 있도록 백업할 앨범을 선택하세요.",
|
||||||
"home_page_share_err_local": "기기의 항목은 링크로 공유할 수 없습니다. 건너뜁니다.",
|
"home_page_share_err_local": "기기의 항목은 링크로 공유할 수 없습니다. 건너뜁니다.",
|
||||||
"home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.",
|
"home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.",
|
||||||
"image_saved_successfully": "이미지 저장",
|
"image_saved_successfully": "이미지가 저장되었습니다.",
|
||||||
"image_viewer_page_state_provider_download_error": "다운로드 오류",
|
"image_viewer_page_state_provider_download_error": "다운로드 오류",
|
||||||
"image_viewer_page_state_provider_download_started": "다운로드가 시작되었습니다.",
|
"image_viewer_page_state_provider_download_started": "다운로드가 시작되었습니다.",
|
||||||
"image_viewer_page_state_provider_download_success": "다운로드 완료",
|
"image_viewer_page_state_provider_download_success": "다운로드 완료",
|
||||||
@ -282,14 +286,14 @@
|
|||||||
"login_form_back_button_text": "뒤로",
|
"login_form_back_button_text": "뒤로",
|
||||||
"login_form_button_text": "로그인",
|
"login_form_button_text": "로그인",
|
||||||
"login_form_email_hint": "youremail@email.com",
|
"login_form_email_hint": "youremail@email.com",
|
||||||
"login_form_endpoint_hint": "https://your-server-ip:port/api",
|
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||||
"login_form_endpoint_url": "서버 엔드포인트 URL",
|
"login_form_endpoint_url": "서버 엔드포인트 URL",
|
||||||
"login_form_err_http": "http:// 또는 https://로 시작해야 합니다.",
|
"login_form_err_http": "http:// 또는 https://로 시작해야 합니다.",
|
||||||
"login_form_err_invalid_email": "유효하지 않은 이메일",
|
"login_form_err_invalid_email": "유효하지 않은 이메일",
|
||||||
"login_form_err_invalid_url": "잘못된 URL입니다.",
|
"login_form_err_invalid_url": "잘못된 URL입니다.",
|
||||||
"login_form_err_leading_whitespace": "앞에 공백 문자가 있습니다.",
|
"login_form_err_leading_whitespace": "시작 부분에 공백이 있습니다.",
|
||||||
"login_form_err_trailing_whitespace": "뒤에 공백 문자가 있습니다.",
|
"login_form_err_trailing_whitespace": "끝 부분에 공백이 있습니다.",
|
||||||
"login_form_failed_get_oauth_server_config": "OAuth 로그인 중 문제 발생, 서버 URL을 확인해주세요.",
|
"login_form_failed_get_oauth_server_config": "OAuth 로그인 중 문제 발생, 서버 URL을 확인하세요.",
|
||||||
"login_form_failed_get_oauth_server_disable": "이 서버는 OAuth 기능을 지원하지 않습니다.",
|
"login_form_failed_get_oauth_server_disable": "이 서버는 OAuth 기능을 지원하지 않습니다.",
|
||||||
"login_form_failed_login": "로그인 오류. 서버 URL, 이메일 및 비밀번호를 확인하세요.",
|
"login_form_failed_login": "로그인 오류. 서버 URL, 이메일 및 비밀번호를 확인하세요.",
|
||||||
"login_form_handshake_exception": "서버와 통신 중 인증서 예외가 발생했습니다. 자체 서명된 인증서를 사용 중이라면, 설정에서 자체 서명된 인증서 허용을 활성화하세요.",
|
"login_form_handshake_exception": "서버와 통신 중 인증서 예외가 발생했습니다. 자체 서명된 인증서를 사용 중이라면, 설정에서 자체 서명된 인증서 허용을 활성화하세요.",
|
||||||
@ -343,7 +347,7 @@
|
|||||||
"notification_permission_dialog_cancel": "취소",
|
"notification_permission_dialog_cancel": "취소",
|
||||||
"notification_permission_dialog_content": "알림을 활성화하려면 설정에서 알림 권한을 허용하세요.",
|
"notification_permission_dialog_content": "알림을 활성화하려면 설정에서 알림 권한을 허용하세요.",
|
||||||
"notification_permission_dialog_settings": "설정",
|
"notification_permission_dialog_settings": "설정",
|
||||||
"notification_permission_list_tile_content": "알림을 활성화하기 위해 권한을 부여하세요.",
|
"notification_permission_list_tile_content": "알림을 활성화하려면 권한을 부여하세요.",
|
||||||
"notification_permission_list_tile_enable_button": "알림 활성화",
|
"notification_permission_list_tile_enable_button": "알림 활성화",
|
||||||
"notification_permission_list_tile_title": "알림 권한",
|
"notification_permission_list_tile_title": "알림 권한",
|
||||||
"partner_list_user_photos": "{user}님의 사진",
|
"partner_list_user_photos": "{user}님의 사진",
|
||||||
@ -371,7 +375,7 @@
|
|||||||
"profile_drawer_app_logs": "로그",
|
"profile_drawer_app_logs": "로그",
|
||||||
"profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.",
|
"profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.",
|
||||||
"profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.",
|
"profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.",
|
||||||
"profile_drawer_client_server_up_to_date": "모바일 앱과 서버가 최신 버전입니다.",
|
"profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신입니다.",
|
||||||
"profile_drawer_documentation": "문서",
|
"profile_drawer_documentation": "문서",
|
||||||
"profile_drawer_github": "Github",
|
"profile_drawer_github": "Github",
|
||||||
"profile_drawer_server_out_of_date_major": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.",
|
"profile_drawer_server_out_of_date_major": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.",
|
||||||
@ -389,7 +393,7 @@
|
|||||||
"search_filter_camera_model": "모델명",
|
"search_filter_camera_model": "모델명",
|
||||||
"search_filter_camera_title": "카메라 종류 선택",
|
"search_filter_camera_title": "카메라 종류 선택",
|
||||||
"search_filter_date": "날짜",
|
"search_filter_date": "날짜",
|
||||||
"search_filter_date_interval": "{start}에서 {end} 까지",
|
"search_filter_date_interval": "{start} - {end}",
|
||||||
"search_filter_date_title": "날짜 범위 선택",
|
"search_filter_date_title": "날짜 범위 선택",
|
||||||
"search_filter_display_option_archive": "보관함",
|
"search_filter_display_option_archive": "보관함",
|
||||||
"search_filter_display_option_favorite": "즐겨찾기",
|
"search_filter_display_option_favorite": "즐겨찾기",
|
||||||
@ -411,8 +415,8 @@
|
|||||||
"search_page_categories": "분류",
|
"search_page_categories": "분류",
|
||||||
"search_page_favorites": "즐겨찾기",
|
"search_page_favorites": "즐겨찾기",
|
||||||
"search_page_motion_photos": "모션 포토",
|
"search_page_motion_photos": "모션 포토",
|
||||||
"search_page_no_objects": "사물 정보가 없습니다.",
|
"search_page_no_objects": "사용 가능한 사물 정보 없음",
|
||||||
"search_page_no_places": "장소 정보가 없습니다.",
|
"search_page_no_places": "사용 가능한 위치 정보 없음",
|
||||||
"search_page_people": "인물",
|
"search_page_people": "인물",
|
||||||
"search_page_person_add_name_dialog_cancel": "취소",
|
"search_page_person_add_name_dialog_cancel": "취소",
|
||||||
"search_page_person_add_name_dialog_hint": "이름",
|
"search_page_person_add_name_dialog_hint": "이름",
|
||||||
@ -435,7 +439,7 @@
|
|||||||
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
|
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
|
||||||
"select_additional_user_for_sharing_page_suggestions": "추천",
|
"select_additional_user_for_sharing_page_suggestions": "추천",
|
||||||
"select_user_for_sharing_page_err_album": "앨범을 생성하지 못했습니다.",
|
"select_user_for_sharing_page_err_album": "앨범을 생성하지 못했습니다.",
|
||||||
"select_user_for_sharing_page_share_suggestions": "추천",
|
"select_user_for_sharing_page_share_suggestions": "제안",
|
||||||
"server_info_box_app_version": "앱 버전",
|
"server_info_box_app_version": "앱 버전",
|
||||||
"server_info_box_latest_release": "최신 버전",
|
"server_info_box_latest_release": "최신 버전",
|
||||||
"server_info_box_server_url": "서버 URL",
|
"server_info_box_server_url": "서버 URL",
|
||||||
@ -454,12 +458,12 @@
|
|||||||
"setting_notifications_notify_minutes": "{}분 후",
|
"setting_notifications_notify_minutes": "{}분 후",
|
||||||
"setting_notifications_notify_never": "알리지 않음",
|
"setting_notifications_notify_never": "알리지 않음",
|
||||||
"setting_notifications_notify_seconds": "{}초",
|
"setting_notifications_notify_seconds": "{}초",
|
||||||
"setting_notifications_single_progress_subtitle": "각 항목의 세부 업로드 정보 표시",
|
"setting_notifications_single_progress_subtitle": "개별 항목의 상세 업로드 정보 표시",
|
||||||
"setting_notifications_single_progress_title": "백그라운드 작업의 세부 진행률 표시",
|
"setting_notifications_single_progress_title": "백그라운드 백업 상세 진행률 표시",
|
||||||
"setting_notifications_subtitle": "알림 기본 설정 조정",
|
"setting_notifications_subtitle": "알림 기본 설정 조정",
|
||||||
"setting_notifications_title": "알림",
|
"setting_notifications_title": "알림",
|
||||||
"setting_notifications_total_progress_subtitle": "전체 업로드 진행률 (완료/전체)",
|
"setting_notifications_total_progress_subtitle": "전체 업로드 진행률 (완료/전체)",
|
||||||
"setting_notifications_total_progress_title": "백그라운드 작업의 전체 진행률 표시",
|
"setting_notifications_total_progress_title": "백그라운드 백업 전체 진행률 표시",
|
||||||
"setting_pages_app_bar_settings": "설정",
|
"setting_pages_app_bar_settings": "설정",
|
||||||
"settings_require_restart": "설정을 적용하려면 Immich를 다시 시작하세요.",
|
"settings_require_restart": "설정을 적용하려면 Immich를 다시 시작하세요.",
|
||||||
"setting_video_viewer_looping_subtitle": "상세 보기에서 동영상을 자동으로 반복합니다.",
|
"setting_video_viewer_looping_subtitle": "상세 보기에서 동영상을 자동으로 반복합니다.",
|
||||||
@ -467,7 +471,7 @@
|
|||||||
"setting_video_viewer_title": "동영상",
|
"setting_video_viewer_title": "동영상",
|
||||||
"share_add": "추가",
|
"share_add": "추가",
|
||||||
"share_add_photos": "사진 추가",
|
"share_add_photos": "사진 추가",
|
||||||
"share_add_title": "앨범 제목 입력",
|
"share_add_title": "앨범명 추가",
|
||||||
"share_assets_selected": "{}개 항목 선택됨",
|
"share_assets_selected": "{}개 항목 선택됨",
|
||||||
"share_create_album": "앨범 생성",
|
"share_create_album": "앨범 생성",
|
||||||
"shared_album_activities_input_disable": "댓글이 비활성화되었습니다",
|
"shared_album_activities_input_disable": "댓글이 비활성화되었습니다",
|
||||||
@ -528,7 +532,7 @@
|
|||||||
"shared_link_manage_links": "공유 링크 관리",
|
"shared_link_manage_links": "공유 링크 관리",
|
||||||
"shared_link_public_album": "공개 앨범",
|
"shared_link_public_album": "공개 앨범",
|
||||||
"share_done": "완료",
|
"share_done": "완료",
|
||||||
"share_invite": "앨범에 초대",
|
"share_invite": "앨범으로 초대",
|
||||||
"sharing_page_album": "공유 앨범",
|
"sharing_page_album": "공유 앨범",
|
||||||
"sharing_page_description": "공유 앨범을 만들어 주변 사람들과 사진 및 동영상을 공유하세요.",
|
"sharing_page_description": "공유 앨범을 만들어 주변 사람들과 사진 및 동영상을 공유하세요.",
|
||||||
"sharing_page_empty_list": "공유 앨범 없음",
|
"sharing_page_empty_list": "공유 앨범 없음",
|
||||||
@ -537,8 +541,8 @@
|
|||||||
"sharing_silver_appbar_share_partner": "파트너와 공유",
|
"sharing_silver_appbar_share_partner": "파트너와 공유",
|
||||||
"sync": "동기화",
|
"sync": "동기화",
|
||||||
"sync_albums": "앨범 동기화",
|
"sync_albums": "앨범 동기화",
|
||||||
"sync_albums_manual_subtitle": "업로드한 모든 동영상과 사진을 선택한 백업 앨범에 동기화합니다.",
|
"sync_albums_manual_subtitle": "업로드한 모든 동영상과 사진을 선택한 백업 앨범에 동기화",
|
||||||
"sync_upload_album_setting_subtitle": "Immich에서 선택한 앨범에 사진 및 동영상을 만들고 업로드하세요.",
|
"sync_upload_album_setting_subtitle": "선택한 앨범을 Immich에 생성하고 사진 및 동영상을 업로드하세요.",
|
||||||
"tab_controller_nav_library": "라이브러리",
|
"tab_controller_nav_library": "라이브러리",
|
||||||
"tab_controller_nav_photos": "사진",
|
"tab_controller_nav_photos": "사진",
|
||||||
"tab_controller_nav_search": "검색",
|
"tab_controller_nav_search": "검색",
|
||||||
@ -546,7 +550,7 @@
|
|||||||
"theme_setting_asset_list_storage_indicator_title": "항목에 스토리지 동기화 여부 표시",
|
"theme_setting_asset_list_storage_indicator_title": "항목에 스토리지 동기화 여부 표시",
|
||||||
"theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 항목 수 ({})",
|
"theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 항목 수 ({})",
|
||||||
"theme_setting_colorful_interface_subtitle": "배경에 대표 색상을 적용합니다.",
|
"theme_setting_colorful_interface_subtitle": "배경에 대표 색상을 적용합니다.",
|
||||||
"theme_setting_colorful_interface_title": "컬러풀 인터페이스",
|
"theme_setting_colorful_interface_title": "미려한 인터페이스",
|
||||||
"theme_setting_dark_mode_switch": "다크 모드",
|
"theme_setting_dark_mode_switch": "다크 모드",
|
||||||
"theme_setting_image_viewer_quality_subtitle": "상세 보기 이미지 품질 조정",
|
"theme_setting_image_viewer_quality_subtitle": "상세 보기 이미지 품질 조정",
|
||||||
"theme_setting_image_viewer_quality_title": "이미지 보기 품질",
|
"theme_setting_image_viewer_quality_title": "이미지 보기 품질",
|
||||||
@ -559,7 +563,7 @@
|
|||||||
"theme_setting_three_stage_loading_subtitle": "이 기능은 앱의 로드 성능을 향상시킬 수 있지만 더 많은 데이터를 사용합니다.",
|
"theme_setting_three_stage_loading_subtitle": "이 기능은 앱의 로드 성능을 향상시킬 수 있지만 더 많은 데이터를 사용합니다.",
|
||||||
"theme_setting_three_stage_loading_title": "3단계 로드 활성화",
|
"theme_setting_three_stage_loading_title": "3단계 로드 활성화",
|
||||||
"translated_text_options": "옵션",
|
"translated_text_options": "옵션",
|
||||||
"trash_emptied": "휴지통 비우기",
|
"trash_emptied": "휴지통을 비움",
|
||||||
"trash_page_delete": "삭제",
|
"trash_page_delete": "삭제",
|
||||||
"trash_page_delete_all": "모두 삭제",
|
"trash_page_delete_all": "모두 삭제",
|
||||||
"trash_page_empty_trash_btn": "휴지통 비우기",
|
"trash_page_empty_trash_btn": "휴지통 비우기",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Dzēst Kopīgošanas saiti",
|
"delete_shared_link_dialog_title": "Dzēst Kopīgošanas saiti",
|
||||||
"description_input_hint_text": "Pievienot aprakstu...",
|
"description_input_hint_text": "Pievienot aprakstu...",
|
||||||
"description_input_submit_error": "Atjauninot aprakstu, radās kļūda; papildinformāciju skatiet žurnālā",
|
"description_input_submit_error": "Atjauninot aprakstu, radās kļūda; papildinformāciju skatiet žurnālā",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Datums un Laiks",
|
"edit_date_time_dialog_date_time": "Datums un Laiks",
|
||||||
"edit_date_time_dialog_timezone": "Laika zona",
|
"edit_date_time_dialog_timezone": "Laika zona",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Slett delt link",
|
"delete_shared_link_dialog_title": "Slett delt link",
|
||||||
"description_input_hint_text": "Legg til beskrivelse ...",
|
"description_input_hint_text": "Legg til beskrivelse ...",
|
||||||
"description_input_submit_error": "Feil ved oppdatering av beskrivelse, sjekk loggen for flere detaljer",
|
"description_input_submit_error": "Feil ved oppdatering av beskrivelse, sjekk loggen for flere detaljer",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Dato og tid",
|
"edit_date_time_dialog_date_time": "Dato og tid",
|
||||||
"edit_date_time_dialog_timezone": "Tidssone",
|
"edit_date_time_dialog_timezone": "Tidssone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Verwijder gedeelde link",
|
"delete_shared_link_dialog_title": "Verwijder gedeelde link",
|
||||||
"description_input_hint_text": "Beschrijving toevoegen...",
|
"description_input_hint_text": "Beschrijving toevoegen...",
|
||||||
"description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details",
|
"description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details",
|
||||||
|
"download_error": "Fout bij downloaden",
|
||||||
|
"download_started": "Download gestart",
|
||||||
|
"download_sucess": "Succesvol gedownload",
|
||||||
|
"download_sucess_android": "Het bestand is gedownload naar DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Datum en tijd",
|
"edit_date_time_dialog_date_time": "Datum en tijd",
|
||||||
"edit_date_time_dialog_timezone": "Tijdzone",
|
"edit_date_time_dialog_timezone": "Tijdzone",
|
||||||
"edit_image_title": "Bewerken",
|
"edit_image_title": "Bewerken",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Usuń udostępniony link",
|
"delete_shared_link_dialog_title": "Usuń udostępniony link",
|
||||||
"description_input_hint_text": "Dodaj opis...",
|
"description_input_hint_text": "Dodaj opis...",
|
||||||
"description_input_submit_error": "Błąd aktualizacji opisu, sprawdź dziennik, aby uzyskać więcej szczegółów",
|
"description_input_submit_error": "Błąd aktualizacji opisu, sprawdź dziennik, aby uzyskać więcej szczegółów",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Data i godzina",
|
"edit_date_time_dialog_date_time": "Data i godzina",
|
||||||
"edit_date_time_dialog_timezone": "Strefa czasowa",
|
"edit_date_time_dialog_timezone": "Strefa czasowa",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Excluir link compartilhado",
|
"delete_shared_link_dialog_title": "Excluir link compartilhado",
|
||||||
"description_input_hint_text": "Adicionar descrição...",
|
"description_input_hint_text": "Adicionar descrição...",
|
||||||
"description_input_submit_error": "Erro ao atualizar a descrição, verifique o registo para obter mais detalhes",
|
"description_input_submit_error": "Erro ao atualizar a descrição, verifique o registo para obter mais detalhes",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Data e Hora",
|
"edit_date_time_dialog_date_time": "Data e Hora",
|
||||||
"edit_date_time_dialog_timezone": "Fuso horário",
|
"edit_date_time_dialog_timezone": "Fuso horário",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Șterge link distribuire",
|
"delete_shared_link_dialog_title": "Șterge link distribuire",
|
||||||
"description_input_hint_text": "Adaugă descriere...",
|
"description_input_hint_text": "Adaugă descriere...",
|
||||||
"description_input_submit_error": "Eroare actualizare descriere, verifică log-urile pentru mai multe detalii",
|
"description_input_submit_error": "Eroare actualizare descriere, verifică log-urile pentru mai multe detalii",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Dată și Oră",
|
"edit_date_time_dialog_date_time": "Dată și Oră",
|
||||||
"edit_date_time_dialog_timezone": "Fus orar",
|
"edit_date_time_dialog_timezone": "Fus orar",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Удалить общую ссылку",
|
"delete_shared_link_dialog_title": "Удалить общую ссылку",
|
||||||
"description_input_hint_text": "Добавить описание...",
|
"description_input_hint_text": "Добавить описание...",
|
||||||
"description_input_submit_error": "Не удалось обновить описание, проверьте логи, чтобы узнать причину",
|
"description_input_submit_error": "Не удалось обновить описание, проверьте логи, чтобы узнать причину",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Дата и время",
|
"edit_date_time_dialog_date_time": "Дата и время",
|
||||||
"edit_date_time_dialog_timezone": "Часовой пояс",
|
"edit_date_time_dialog_timezone": "Часовой пояс",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Odstrániť zdieľaný odkaz",
|
"delete_shared_link_dialog_title": "Odstrániť zdieľaný odkaz",
|
||||||
"description_input_hint_text": "Pridať popis...",
|
"description_input_hint_text": "Pridať popis...",
|
||||||
"description_input_submit_error": "Chyba pri aktualizovaní popisu, zobrazte log pre viac detailov",
|
"description_input_submit_error": "Chyba pri aktualizovaní popisu, zobrazte log pre viac detailov",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Dátum a čas",
|
"edit_date_time_dialog_date_time": "Dátum a čas",
|
||||||
"edit_date_time_dialog_timezone": "Časové pásmo",
|
"edit_date_time_dialog_timezone": "Časové pásmo",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Izbriši povezavo skupne rabe",
|
"delete_shared_link_dialog_title": "Izbriši povezavo skupne rabe",
|
||||||
"description_input_hint_text": "Dodaj opis ...",
|
"description_input_hint_text": "Dodaj opis ...",
|
||||||
"description_input_submit_error": "Napaka pri posodabljanju opisa, preverite dnevnik za več podrobnosti",
|
"description_input_submit_error": "Napaka pri posodabljanju opisa, preverite dnevnik za več podrobnosti",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Datum in ura",
|
"edit_date_time_dialog_date_time": "Datum in ura",
|
||||||
"edit_date_time_dialog_timezone": "Časovni pas",
|
"edit_date_time_dialog_timezone": "Časovni pas",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Ta Bort Delad Länk",
|
"delete_shared_link_dialog_title": "Ta Bort Delad Länk",
|
||||||
"description_input_hint_text": "Lägg till beskrivning...",
|
"description_input_hint_text": "Lägg till beskrivning...",
|
||||||
"description_input_submit_error": "Fel vid uppdatering av beskrivning, se loggen för fler detaljer",
|
"description_input_submit_error": "Fel vid uppdatering av beskrivning, se loggen för fler detaljer",
|
||||||
|
"download_error": "Fel vid nedladdning",
|
||||||
|
"download_started": "Nedladdning påbörjad",
|
||||||
|
"download_sucess": "Nedladdning lyckades",
|
||||||
|
"download_sucess_android": "Media har laddats ner till DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Datum och Tid",
|
"edit_date_time_dialog_date_time": "Datum och Tid",
|
||||||
"edit_date_time_dialog_timezone": "Tidszon",
|
"edit_date_time_dialog_timezone": "Tidszon",
|
||||||
"edit_image_title": "Redigera",
|
"edit_image_title": "Redigera",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "ลบลิงก์ที่แชร์",
|
"delete_shared_link_dialog_title": "ลบลิงก์ที่แชร์",
|
||||||
"description_input_hint_text": "เพื่มรายละเอียด...",
|
"description_input_hint_text": "เพื่มรายละเอียด...",
|
||||||
"description_input_submit_error": "อัพเดตรายละเอียดผิดพลาด ตรวจสอบ log เพื่อรายละเอียดเพิ่มเติม",
|
"description_input_submit_error": "อัพเดตรายละเอียดผิดพลาด ตรวจสอบ log เพื่อรายละเอียดเพิ่มเติม",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "วันและเวลา",
|
"edit_date_time_dialog_date_time": "วันและเวลา",
|
||||||
"edit_date_time_dialog_timezone": "เขดเวลา",
|
"edit_date_time_dialog_timezone": "เขดเวลา",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Видалити спільне посилання",
|
"delete_shared_link_dialog_title": "Видалити спільне посилання",
|
||||||
"description_input_hint_text": "Додати опис...",
|
"description_input_hint_text": "Додати опис...",
|
||||||
"description_input_submit_error": "Помилка оновлення опису, перевірте логи для подробиць",
|
"description_input_submit_error": "Помилка оновлення опису, перевірте логи для подробиць",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Дата і час",
|
"edit_date_time_dialog_date_time": "Дата і час",
|
||||||
"edit_date_time_dialog_timezone": "Часовий пояс",
|
"edit_date_time_dialog_timezone": "Часовий пояс",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
"backup_controller_page_background_turn_on": "Bật dịch vụ nền",
|
"backup_controller_page_background_turn_on": "Bật dịch vụ nền",
|
||||||
"backup_controller_page_background_wifi": "Chỉ khi dùng Wi-Fi",
|
"backup_controller_page_background_wifi": "Chỉ khi dùng Wi-Fi",
|
||||||
"backup_controller_page_backup": "Sao lưu",
|
"backup_controller_page_backup": "Sao lưu",
|
||||||
"backup_controller_page_backup_selected": "Đã chọn:",
|
"backup_controller_page_backup_selected": "Đã chọn: ",
|
||||||
"backup_controller_page_backup_sub": "Ảnh và video đã sao lưu",
|
"backup_controller_page_backup_sub": "Ảnh và video đã sao lưu",
|
||||||
"backup_controller_page_cancel": "Từ chối",
|
"backup_controller_page_cancel": "Từ chối",
|
||||||
"backup_controller_page_created": "Tạo vào: {}",
|
"backup_controller_page_created": "Tạo vào: {}",
|
||||||
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Xoá liên kết đã chia sẻ",
|
"delete_shared_link_dialog_title": "Xoá liên kết đã chia sẻ",
|
||||||
"description_input_hint_text": "Thêm mô tả...",
|
"description_input_hint_text": "Thêm mô tả...",
|
||||||
"description_input_submit_error": "Cập nhật mô tả không thành công, vui lòng kiểm tra nhật ký để biết thêm chi tiết",
|
"description_input_submit_error": "Cập nhật mô tả không thành công, vui lòng kiểm tra nhật ký để biết thêm chi tiết",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Ngày và Giờ",
|
"edit_date_time_dialog_date_time": "Ngày và Giờ",
|
||||||
"edit_date_time_dialog_timezone": "Múi giờ",
|
"edit_date_time_dialog_timezone": "Múi giờ",
|
||||||
"edit_image_title": "Sửa",
|
"edit_image_title": "Sửa",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "删除共享链接",
|
"delete_shared_link_dialog_title": "删除共享链接",
|
||||||
"description_input_hint_text": "添加描述...",
|
"description_input_hint_text": "添加描述...",
|
||||||
"description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息",
|
"description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息",
|
||||||
|
"download_error": "下载出错",
|
||||||
|
"download_started": "开始下载",
|
||||||
|
"download_sucess": "下载成功",
|
||||||
|
"download_sucess_android": "媒体已下载至 DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "日期和时间",
|
"edit_date_time_dialog_date_time": "日期和时间",
|
||||||
"edit_date_time_dialog_timezone": "时区",
|
"edit_date_time_dialog_timezone": "时区",
|
||||||
"edit_image_title": "编辑",
|
"edit_image_title": "编辑",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "删除共享链接",
|
"delete_shared_link_dialog_title": "删除共享链接",
|
||||||
"description_input_hint_text": "添加描述...",
|
"description_input_hint_text": "添加描述...",
|
||||||
"description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息",
|
"description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息",
|
||||||
|
"download_error": "下载出错",
|
||||||
|
"download_started": "开始下载",
|
||||||
|
"download_sucess": "下载成功",
|
||||||
|
"download_sucess_android": "媒体已下载至 DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "日期和时间",
|
"edit_date_time_dialog_date_time": "日期和时间",
|
||||||
"edit_date_time_dialog_timezone": "时区",
|
"edit_date_time_dialog_timezone": "时区",
|
||||||
"edit_image_title": "编辑",
|
"edit_image_title": "编辑",
|
||||||
|
@ -210,6 +210,10 @@
|
|||||||
"delete_shared_link_dialog_title": "Delete Shared Link",
|
"delete_shared_link_dialog_title": "Delete Shared Link",
|
||||||
"description_input_hint_text": "Add description...",
|
"description_input_hint_text": "Add description...",
|
||||||
"description_input_submit_error": "Error updating description, check the log for more details",
|
"description_input_submit_error": "Error updating description, check the log for more details",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"edit_date_time_dialog_date_time": "Date and Time",
|
"edit_date_time_dialog_date_time": "Date and Time",
|
||||||
"edit_date_time_dialog_timezone": "Timezone",
|
"edit_date_time_dialog_timezone": "Timezone",
|
||||||
"edit_image_title": "Edit",
|
"edit_image_title": "Edit",
|
||||||
|
@ -401,7 +401,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 172;
|
CURRENT_PROJECT_VERSION = 173;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -543,7 +543,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 172;
|
CURRENT_PROJECT_VERSION = 173;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -571,7 +571,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 172;
|
CURRENT_PROJECT_VERSION = 173;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
@ -58,11 +58,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.113.1</string>
|
<string>1.114.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>172</string>
|
<string>173</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
@ -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.113.1"
|
version_number: "1.114.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -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.113.1
|
- API version: 1.114.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
|
||||||
|
|
||||||
|
@ -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.113.1+157
|
version: 1.114.0+158
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
@ -7394,7 +7394,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.113.1",
|
"version": "1.114.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.113.1",
|
"version": "1.114.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.113.1",
|
"version": "1.114.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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.113.1",
|
"version": "1.114.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",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.113.1
|
* 1.114.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
|
||||||
*/
|
*/
|
||||||
|
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.113.1",
|
"version": "1.114.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.113.1",
|
"version": "1.114.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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.113.1",
|
"version": "1.114.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -75,6 +75,7 @@ export const supportedPresetTokens = [
|
|||||||
'{{y}}/{{MM}}-{{dd}}/{{filename}}',
|
'{{y}}/{{MM}}-{{dd}}/{{filename}}',
|
||||||
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
||||||
'{{y}}/{{MM}}/{{filename}}',
|
'{{y}}/{{MM}}/{{filename}}',
|
||||||
|
'{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
|
||||||
'{{y}}/{{MMM}}/{{filename}}',
|
'{{y}}/{{MMM}}/{{filename}}',
|
||||||
'{{y}}/{{MMMM}}/{{filename}}',
|
'{{y}}/{{MMMM}}/{{filename}}',
|
||||||
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
||||||
|
@ -12,12 +12,14 @@ export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadat
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string };
|
export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string };
|
||||||
|
export type SystemFlags = { mountFiles: boolean };
|
||||||
|
|
||||||
export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, any>> {
|
export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, any>> {
|
||||||
[SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
|
|
||||||
[SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string };
|
|
||||||
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
|
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
|
||||||
[SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>;
|
[SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string };
|
||||||
[SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata;
|
|
||||||
[SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date };
|
[SystemMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date };
|
||||||
|
[SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
|
||||||
|
[SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>;
|
||||||
|
[SystemMetadataKey.SYSTEM_FLAGS]: SystemFlags;
|
||||||
|
[SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata;
|
||||||
}
|
}
|
||||||
|
@ -153,6 +153,7 @@ export enum SystemMetadataKey {
|
|||||||
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
|
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
|
||||||
ADMIN_ONBOARDING = 'admin-onboarding',
|
ADMIN_ONBOARDING = 'admin-onboarding',
|
||||||
SYSTEM_CONFIG = 'system-config',
|
SYSTEM_CONFIG = 'system-config',
|
||||||
|
SYSTEM_FLAGS = 'system-flags',
|
||||||
VERSION_CHECK_STATE = 'version-check-state',
|
VERSION_CHECK_STATE = 'version-check-state',
|
||||||
LICENSE = 'license',
|
LICENSE = 'license',
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ export enum VectorIndex {
|
|||||||
export enum DatabaseLock {
|
export enum DatabaseLock {
|
||||||
GeodataImport = 100,
|
GeodataImport = 100,
|
||||||
Migrations = 200,
|
Migrations = 200,
|
||||||
|
SystemFileMounts = 300,
|
||||||
StorageTemplateMigration = 420,
|
StorageTemplateMigration = 420,
|
||||||
CLIPDimSize = 512,
|
CLIPDimSize = 512,
|
||||||
LibraryWatch = 1337,
|
LibraryWatch = 1337,
|
||||||
|
@ -21,6 +21,9 @@ type EmitEventMap = {
|
|||||||
'asset.tag': [{ assetId: string }];
|
'asset.tag': [{ assetId: string }];
|
||||||
'asset.untag': [{ assetId: string }];
|
'asset.untag': [{ assetId: string }];
|
||||||
|
|
||||||
|
// session events
|
||||||
|
'session.delete': [{ sessionId: string }];
|
||||||
|
|
||||||
// user events
|
// user events
|
||||||
'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
|
'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
|
||||||
};
|
};
|
||||||
@ -43,6 +46,7 @@ export enum ClientEvent {
|
|||||||
SERVER_VERSION = 'on_server_version',
|
SERVER_VERSION = 'on_server_version',
|
||||||
CONFIG_UPDATE = 'on_config_update',
|
CONFIG_UPDATE = 'on_config_update',
|
||||||
NEW_RELEASE = 'on_new_release',
|
NEW_RELEASE = 'on_new_release',
|
||||||
|
SESSION_DELETE = 'on_session_delete',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientEventMap {
|
export interface ClientEventMap {
|
||||||
@ -58,6 +62,7 @@ export interface ClientEventMap {
|
|||||||
[ClientEvent.SERVER_VERSION]: ServerVersionResponseDto;
|
[ClientEvent.SERVER_VERSION]: ServerVersionResponseDto;
|
||||||
[ClientEvent.CONFIG_UPDATE]: Record<string, never>;
|
[ClientEvent.CONFIG_UPDATE]: Record<string, never>;
|
||||||
[ClientEvent.NEW_RELEASE]: ReleaseNotification;
|
[ClientEvent.NEW_RELEASE]: ReleaseNotification;
|
||||||
|
[ClientEvent.SESSION_DELETE]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ServerEvent {
|
export enum ServerEvent {
|
||||||
@ -77,7 +82,7 @@ export interface IEventRepository {
|
|||||||
/**
|
/**
|
||||||
* Send to connected clients for a specific user
|
* Send to connected clients for a specific user
|
||||||
*/
|
*/
|
||||||
clientSend<E extends keyof ClientEventMap>(event: E, userId: string, data: ClientEventMap[E]): void;
|
clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]): void;
|
||||||
/**
|
/**
|
||||||
* Send to all connected clients
|
* Send to all connected clients
|
||||||
*/
|
*/
|
||||||
|
@ -26,7 +26,7 @@ export interface MapMarker extends ReverseGeocodeResult {
|
|||||||
|
|
||||||
export interface IMapRepository {
|
export interface IMapRepository {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult | null>;
|
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
||||||
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||||
fetchStyle(url: string): Promise<any>;
|
fetchStyle(url: string): Promise<any>;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
|||||||
|
|
||||||
export interface IMetadataRepository {
|
export interface IMetadataRepository {
|
||||||
teardown(): Promise<void>;
|
teardown(): Promise<void>;
|
||||||
readTags(path: string): Promise<ImmichTags | null>;
|
readTags(path: string): Promise<ImmichTags>;
|
||||||
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
||||||
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
||||||
getCountries(userIds: string[]): Promise<Array<string | null>>;
|
getCountries(userIds: string[]): Promise<Array<string | null>>;
|
||||||
|
@ -17,7 +17,13 @@ async function bootstrapImmichAdmin() {
|
|||||||
|
|
||||||
function bootstrapWorker(name: string) {
|
function bootstrapWorker(name: string) {
|
||||||
console.log(`Starting ${name} worker`);
|
console.log(`Starting ${name} worker`);
|
||||||
|
|
||||||
const worker = name === 'api' ? fork(`./dist/workers/${name}.js`) : new Worker(`./dist/workers/${name}.js`);
|
const worker = name === 'api' ? fork(`./dist/workers/${name}.js`) : new Worker(`./dist/workers/${name}.js`);
|
||||||
|
|
||||||
|
worker.on('error', (error) => {
|
||||||
|
console.error(`${name} worker error: ${error}`);
|
||||||
|
});
|
||||||
|
|
||||||
worker.on('exit', (exitCode) => {
|
worker.on('exit', (exitCode) => {
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
console.error(`${name} worker exited with code ${exitCode}`);
|
console.error(`${name} worker exited with code ${exitCode}`);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import {
|
import {
|
||||||
OnGatewayConnection,
|
OnGatewayConnection,
|
||||||
@ -37,7 +38,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
|||||||
private server?: Server;
|
private server?: Server;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private moduleRef: ModuleRef,
|
||||||
private eventEmitter: EventEmitter2,
|
private eventEmitter: EventEmitter2,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
@ -62,12 +63,15 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
|||||||
async handleConnection(client: Socket) {
|
async handleConnection(client: Socket) {
|
||||||
try {
|
try {
|
||||||
this.logger.log(`Websocket Connect: ${client.id}`);
|
this.logger.log(`Websocket Connect: ${client.id}`);
|
||||||
const auth = await this.authService.authenticate({
|
const auth = await this.moduleRef.get(AuthService).authenticate({
|
||||||
headers: client.request.headers,
|
headers: client.request.headers,
|
||||||
queryParams: {},
|
queryParams: {},
|
||||||
metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' },
|
metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' },
|
||||||
});
|
});
|
||||||
await client.join(auth.user.id);
|
await client.join(auth.user.id);
|
||||||
|
if (auth.session) {
|
||||||
|
await client.join(auth.session.id);
|
||||||
|
}
|
||||||
this.serverSend(ServerEvent.WEBSOCKET_CONNECT, { userId: auth.user.id });
|
this.serverSend(ServerEvent.WEBSOCKET_CONNECT, { userId: auth.user.id });
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
this.logger.error(`Websocket connection error: ${error}`, error?.stack);
|
this.logger.error(`Websocket connection error: ${error}`, error?.stack);
|
||||||
@ -96,8 +100,8 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clientSend<E extends keyof ClientEventMap>(event: E, userId: string, data: ClientEventMap[E]) {
|
clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]) {
|
||||||
this.server?.to(userId).emit(event, data);
|
this.server?.to(room).emit(event, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]) {
|
clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]) {
|
||||||
|
@ -124,7 +124,7 @@ export class MapRepository implements IMapRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult | null> {
|
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
||||||
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
||||||
|
|
||||||
const response = await this.geodataPlacesRepository
|
const response = await this.geodataPlacesRepository
|
||||||
@ -159,7 +159,7 @@ export class MapRepository implements IMapRepository {
|
|||||||
`Response from database for natural earth reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
|
`Response from database for natural earth reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return null;
|
return { country: null, state: null, city: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
|
this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
|
||||||
|
@ -36,11 +36,11 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
await this.exiftool.end();
|
await this.exiftool.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
readTags(path: string): Promise<ImmichTags | null> {
|
readTags(path: string): Promise<ImmichTags> {
|
||||||
return this.exiftool.read(path).catch((error) => {
|
return this.exiftool.read(path).catch((error) => {
|
||||||
this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack);
|
this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack);
|
||||||
return null;
|
return {};
|
||||||
}) as Promise<ImmichTags | null>;
|
}) as Promise<ImmichTags>;
|
||||||
}
|
}
|
||||||
|
|
||||||
extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
|
extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
|
||||||
|
@ -6,6 +6,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
|||||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
|
import { SourceType } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
AssetFaceId,
|
AssetFaceId,
|
||||||
DeleteAllFacesOptions,
|
DeleteAllFacesOptions,
|
||||||
@ -53,16 +54,20 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise<void> {
|
async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise<void> {
|
||||||
if (sourceType) {
|
if (!sourceType) {
|
||||||
|
return this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
|
||||||
|
}
|
||||||
|
|
||||||
await this.assetFaceRepository
|
await this.assetFaceRepository
|
||||||
.createQueryBuilder('asset_faces')
|
.createQueryBuilder('asset_faces')
|
||||||
.delete()
|
.delete()
|
||||||
.andWhere('sourceType = :sourceType', { sourceType })
|
.andWhere('sourceType = :sourceType', { sourceType })
|
||||||
.execute();
|
.execute();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
|
await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search');
|
||||||
|
if (sourceType === SourceType.MACHINE_LEARNING) {
|
||||||
|
await this.assetFaceRepository.query('REINDEX INDEX face_index');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllFaces(
|
getAllFaces(
|
||||||
|
@ -6,6 +6,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
@ -20,6 +21,7 @@ import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
|||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
|
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
|
||||||
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
||||||
@ -56,6 +58,7 @@ const oauthUserWithDefaultQuota = {
|
|||||||
describe('AuthService', () => {
|
describe('AuthService', () => {
|
||||||
let sut: AuthService;
|
let sut: AuthService;
|
||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let userMock: Mocked<IUserRepository>;
|
let userMock: Mocked<IUserRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
@ -87,6 +90,7 @@ describe('AuthService', () => {
|
|||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
eventMock = newEventRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
@ -94,7 +98,7 @@ describe('AuthService', () => {
|
|||||||
shareMock = newSharedLinkRepositoryMock();
|
shareMock = newSharedLinkRepositoryMock();
|
||||||
keyMock = newKeyRepositoryMock();
|
keyMock = newKeyRepositoryMock();
|
||||||
|
|
||||||
sut = new AuthService(cryptoMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock);
|
sut = new AuthService(cryptoMock, eventMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -208,6 +212,7 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(sessionMock.delete).toHaveBeenCalledWith('token123');
|
expect(sessionMock.delete).toHaveBeenCalledWith('token123');
|
||||||
|
expect(eventMock.emit).toHaveBeenCalledWith('session.delete', { sessionId: 'token123' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
|
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
|
||||||
|
@ -34,6 +34,7 @@ import { UserEntity } from 'src/entities/user.entity';
|
|||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
@ -75,6 +76,7 @@ export class AuthService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@ -114,6 +116,7 @@ export class AuthService {
|
|||||||
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
||||||
if (auth.session) {
|
if (auth.session) {
|
||||||
await this.sessionRepository.delete(auth.session.id);
|
await this.sessionRepository.delete(auth.session.id);
|
||||||
|
await this.eventRepository.emit('session.delete', { sessionId: auth.session.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1247,6 +1247,7 @@ describe(MediaService.name, () => {
|
|||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
'-map 0:0',
|
'-map 0:0',
|
||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
|
'-strict unofficial',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf scale=-2:720,format=yuv420p',
|
'-vf scale=-2:720,format=yuv420p',
|
||||||
'-preset 12',
|
'-preset 12',
|
||||||
@ -1372,6 +1373,7 @@ describe(MediaService.name, () => {
|
|||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
'-map 0:0',
|
'-map 0:0',
|
||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
|
'-strict unofficial',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf format=nv12,hwupload_cuda,scale_cuda=-2:720',
|
'-vf format=nv12,hwupload_cuda,scale_cuda=-2:720',
|
||||||
@ -1532,6 +1534,7 @@ describe(MediaService.name, () => {
|
|||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
'-map 0:0',
|
'-map 0:0',
|
||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
|
'-strict unofficial',
|
||||||
'-bf 7',
|
'-bf 7',
|
||||||
'-refs 5',
|
'-refs 5',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
@ -1716,6 +1719,7 @@ describe(MediaService.name, () => {
|
|||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
'-map 0:0',
|
'-map 0:0',
|
||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
|
'-strict unofficial',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf format=nv12,hwupload,scale_vaapi=-2:720',
|
'-vf format=nv12,hwupload,scale_vaapi=-2:720',
|
||||||
@ -1913,6 +1917,7 @@ describe(MediaService.name, () => {
|
|||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
'-map 0:0',
|
'-map 0:0',
|
||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
|
'-strict unofficial',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
|
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
|
||||||
|
@ -522,13 +522,13 @@ describe(MetadataService.name, () => {
|
|||||||
it('should extract the correct video orientation', async () => {
|
it('should extract the correct video orientation', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
metadataMock.readTags.mockResolvedValue(null);
|
metadataMock.readTags.mockResolvedValue({});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
|
||||||
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ orientation: Orientation.Rotate270CW }),
|
expect.objectContaining({ orientation: Orientation.Rotate270CW.toString() }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -814,6 +814,9 @@ describe(MetadataService.name, () => {
|
|||||||
projectionType: 'EQUIRECTANGULAR',
|
projectionType: 'EQUIRECTANGULAR',
|
||||||
timeZone: tags.tz,
|
timeZone: tags.tz,
|
||||||
rating: tags.Rating,
|
rating: tags.Rating,
|
||||||
|
country: null,
|
||||||
|
state: null,
|
||||||
|
city: null,
|
||||||
});
|
});
|
||||||
expect(assetMock.update).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ContainerDirectoryItem, ExifDateTime, Tags } from 'exiftool-vendored';
|
import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored';
|
||||||
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
@ -11,7 +11,6 @@ import { SystemConfigCore } from 'src/cores/system-config.core';
|
|||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { AssetType, SourceType } from 'src/enum';
|
import { AssetType, SourceType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
@ -30,7 +29,7 @@ import {
|
|||||||
QueueName,
|
QueueName,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMapRepository } from 'src/interfaces/map.interface';
|
import { IMapRepository, ReverseGeocodeResult } from 'src/interfaces/map.interface';
|
||||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||||
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
@ -56,23 +55,16 @@ const EXIF_DATE_TAGS: Array<keyof Tags> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export enum Orientation {
|
export enum Orientation {
|
||||||
Horizontal = '1',
|
Horizontal = 1,
|
||||||
MirrorHorizontal = '2',
|
MirrorHorizontal = 2,
|
||||||
Rotate180 = '3',
|
Rotate180 = 3,
|
||||||
MirrorVertical = '4',
|
MirrorVertical = 4,
|
||||||
MirrorHorizontalRotate270CW = '5',
|
MirrorHorizontalRotate270CW = 5,
|
||||||
Rotate90CW = '6',
|
Rotate90CW = 6,
|
||||||
MirrorHorizontalRotate90CW = '7',
|
MirrorHorizontalRotate90CW = 7,
|
||||||
Rotate270CW = '8',
|
Rotate270CW = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExifEntityWithoutGeocodeAndTypeOrm = Omit<ExifEntity, 'city' | 'state' | 'country' | 'description'> & {
|
|
||||||
dateTimeOriginal: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null);
|
|
||||||
const tzOffset = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.tzoffsetMinutes : null);
|
|
||||||
|
|
||||||
const validate = <T>(value: T): NonNullable<T> | null => {
|
const validate = <T>(value: T): NonNullable<T> | null => {
|
||||||
// handle lists of numbers
|
// handle lists of numbers
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@ -218,36 +210,73 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { metadata } = await this.configCore.getConfig({ withCache: true });
|
const { metadata, reverseGeocoding } = await this.configCore.getConfig({ withCache: true });
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { exifData, exifTags } = await this.exifData(asset);
|
const stats = await this.storageRepository.stat(asset.originalPath);
|
||||||
|
|
||||||
if (asset.type === AssetType.VIDEO) {
|
const exifTags = await this.getExifTags(asset);
|
||||||
await this.applyVideoMetadata(asset, exifData);
|
|
||||||
}
|
this.logger.verbose('Exif Tags', exifTags);
|
||||||
|
|
||||||
|
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
|
||||||
|
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
||||||
|
|
||||||
|
const exifData = {
|
||||||
|
assetId: asset.id,
|
||||||
|
|
||||||
|
// dates
|
||||||
|
dateTimeOriginal,
|
||||||
|
modifyDate,
|
||||||
|
timeZone,
|
||||||
|
|
||||||
|
// gps
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
country,
|
||||||
|
state,
|
||||||
|
city,
|
||||||
|
|
||||||
|
// image/file
|
||||||
|
fileSizeInByte: stats.size,
|
||||||
|
exifImageHeight: validate(exifTags.ImageHeight),
|
||||||
|
exifImageWidth: validate(exifTags.ImageWidth),
|
||||||
|
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
||||||
|
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
||||||
|
bitsPerSample: this.getBitsPerSample(exifTags),
|
||||||
|
colorspace: exifTags.ColorSpace ?? null,
|
||||||
|
|
||||||
|
// camera
|
||||||
|
make: exifTags.Make ?? null,
|
||||||
|
model: exifTags.Model ?? null,
|
||||||
|
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
||||||
|
iso: validate(exifTags.ISO),
|
||||||
|
exposureTime: exifTags.ExposureTime ?? null,
|
||||||
|
lensModel: exifTags.LensModel ?? null,
|
||||||
|
fNumber: validate(exifTags.FNumber),
|
||||||
|
focalLength: validate(exifTags.FocalLength),
|
||||||
|
|
||||||
|
// comments
|
||||||
|
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
||||||
|
profileDescription: exifTags.ProfileDescription || null,
|
||||||
|
rating: exifTags.Rating ?? null,
|
||||||
|
|
||||||
|
// grouping
|
||||||
|
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
||||||
|
autoStackId: this.getAutoStackId(exifTags),
|
||||||
|
};
|
||||||
|
|
||||||
await this.applyMotionPhotos(asset, exifTags);
|
|
||||||
await this.applyReverseGeocoding(asset, exifData);
|
|
||||||
await this.applyTagList(asset, exifTags);
|
await this.applyTagList(asset, exifTags);
|
||||||
|
await this.applyMotionPhotos(asset, exifTags);
|
||||||
|
|
||||||
await this.assetRepository.upsertExif(exifData);
|
await this.assetRepository.upsertExif(exifData);
|
||||||
|
|
||||||
const dateTimeOriginal = exifData.dateTimeOriginal;
|
|
||||||
let localDateTime = dateTimeOriginal ?? undefined;
|
|
||||||
|
|
||||||
const timeZoneOffset = tzOffset(firstDateTime(exifTags as Tags)) ?? 0;
|
|
||||||
|
|
||||||
if (dateTimeOriginal && timeZoneOffset) {
|
|
||||||
localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.assetRepository.update({
|
await this.assetRepository.update({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
duration: asset.duration,
|
duration: exifTags.Duration?.toString() ?? null,
|
||||||
localDateTime,
|
localDateTime,
|
||||||
fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
|
fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
|
||||||
});
|
});
|
||||||
@ -338,25 +367,20 @@ export class MetadataService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
private async getExifTags(asset: AssetEntity): Promise<ImmichTags> {
|
||||||
const { latitude, longitude } = exifData;
|
const mediaTags = await this.repository.readTags(asset.originalPath);
|
||||||
const { reverseGeocoding } = await this.configCore.getConfig({ withCache: true });
|
const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : {};
|
||||||
if (!reverseGeocoding.enabled || !longitude || !latitude) {
|
const videoTags = asset.type === AssetType.VIDEO ? await this.getVideoTags(asset.originalPath) : {};
|
||||||
return;
|
|
||||||
|
// make sure dates comes from sidecar
|
||||||
|
const sidecarDate = firstDateTime(sidecarTags as Tags, EXIF_DATE_TAGS);
|
||||||
|
if (sidecarDate) {
|
||||||
|
for (const tag of EXIF_DATE_TAGS) {
|
||||||
|
delete mediaTags[tag];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return { ...mediaTags, ...videoTags, ...sidecarTags };
|
||||||
const reverseGeocode = await this.mapRepository.reverseGeocode({ latitude, longitude });
|
|
||||||
if (!reverseGeocode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object.assign(exifData, reverseGeocode);
|
|
||||||
} catch (error: Error | any) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`,
|
|
||||||
error?.stack,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) {
|
private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) {
|
||||||
@ -576,66 +600,65 @@ export class MetadataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async exifData(
|
private getDates(asset: AssetEntity, exifTags: ImmichTags) {
|
||||||
asset: AssetEntity,
|
const dateTime = firstDateTime(exifTags as Maybe<Tags>, EXIF_DATE_TAGS);
|
||||||
): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; exifTags: ImmichTags }> {
|
this.logger.debug(`Asset ${asset.id} date time is ${dateTime}`);
|
||||||
const stats = await this.storageRepository.stat(asset.originalPath);
|
|
||||||
const mediaTags = await this.repository.readTags(asset.originalPath);
|
|
||||||
const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : null;
|
|
||||||
|
|
||||||
// ensure date from sidecar is used if present
|
// created
|
||||||
const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags);
|
let dateTimeOriginal = dateTime?.toDate();
|
||||||
if (mediaTags && hasDateOverride) {
|
if (!dateTimeOriginal) {
|
||||||
for (const tag of EXIF_DATE_TAGS) {
|
this.logger.warn(`Asset ${asset.id} has no valid date (${dateTime}), falling back to asset.fileCreatedAt`);
|
||||||
delete mediaTags[tag];
|
dateTimeOriginal = asset.fileCreatedAt;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const exifTags = { ...mediaTags, ...sidecarTags };
|
// timezone
|
||||||
|
let timeZone = exifTags.tz ?? null;
|
||||||
|
if (timeZone == null && dateTime?.rawValue?.endsWith('+00:00')) {
|
||||||
|
// exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly
|
||||||
|
// https://github.com/photostructure/exiftool-vendored.js/issues/203
|
||||||
|
timeZone = 'UTC+0';
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.verbose('Exif Tags', exifTags);
|
if (timeZone) {
|
||||||
|
this.logger.debug(`Asset ${asset.id} timezone is ${timeZone} (via ${exifTags.tzSource})`);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Asset ${asset.id} has no time zone information`);
|
||||||
|
}
|
||||||
|
|
||||||
const dateTimeOriginalWithRawValue = this.getDateTimeOriginalWithRawValue(exifTags);
|
// offset minutes
|
||||||
const dateTimeOriginal = dateTimeOriginalWithRawValue.exifDate ?? asset.fileCreatedAt;
|
const offsetMinutes = dateTime?.tzoffsetMinutes || 0;
|
||||||
const timeZone = this.getTimeZone(exifTags, dateTimeOriginalWithRawValue.rawValue);
|
let localDateTime = dateTimeOriginal;
|
||||||
|
if (offsetMinutes) {
|
||||||
|
localDateTime = new Date(dateTimeOriginal.getTime() + offsetMinutes * 60_000);
|
||||||
|
this.logger.debug(`Asset ${asset.id} local time is offset by ${offsetMinutes} minutes`);
|
||||||
|
}
|
||||||
|
|
||||||
const exifData = {
|
return {
|
||||||
// altitude: tags.GPSAltitude ?? null,
|
|
||||||
assetId: asset.id,
|
|
||||||
bitsPerSample: this.getBitsPerSample(exifTags),
|
|
||||||
colorspace: exifTags.ColorSpace ?? null,
|
|
||||||
dateTimeOriginal,
|
dateTimeOriginal,
|
||||||
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
|
||||||
exifImageHeight: validate(exifTags.ImageHeight),
|
|
||||||
exifImageWidth: validate(exifTags.ImageWidth),
|
|
||||||
exposureTime: exifTags.ExposureTime ?? null,
|
|
||||||
fileSizeInByte: stats.size,
|
|
||||||
fNumber: validate(exifTags.FNumber),
|
|
||||||
focalLength: validate(exifTags.FocalLength),
|
|
||||||
fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
|
||||||
iso: validate(exifTags.ISO),
|
|
||||||
latitude: validate(exifTags.GPSLatitude),
|
|
||||||
lensModel: exifTags.LensModel ?? null,
|
|
||||||
livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null,
|
|
||||||
autoStackId: this.getAutoStackId(exifTags),
|
|
||||||
longitude: validate(exifTags.GPSLongitude),
|
|
||||||
make: exifTags.Make ?? null,
|
|
||||||
model: exifTags.Model ?? null,
|
|
||||||
modifyDate: exifDate(exifTags.ModifyDate) ?? asset.fileModifiedAt,
|
|
||||||
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
|
||||||
profileDescription: exifTags.ProfileDescription || null,
|
|
||||||
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
|
||||||
timeZone,
|
timeZone,
|
||||||
rating: exifTags.Rating ?? null,
|
localDateTime,
|
||||||
|
modifyDate: (exifTags.ModifyDate as ExifDateTime)?.toDate() ?? asset.fileModifiedAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (exifData.latitude === 0 && exifData.longitude === 0) {
|
|
||||||
this.logger.warn('Exif data has latitude and longitude of 0, setting to null');
|
|
||||||
exifData.latitude = null;
|
|
||||||
exifData.longitude = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { exifData, exifTags };
|
private async getGeo(tags: ImmichTags, reverseGeocoding: SystemConfig['reverseGeocoding']) {
|
||||||
|
let latitude = validate(tags.GPSLatitude);
|
||||||
|
let longitude = validate(tags.GPSLongitude);
|
||||||
|
|
||||||
|
// TODO take ref into account
|
||||||
|
|
||||||
|
if (latitude === 0 && longitude === 0) {
|
||||||
|
this.logger.warn('Latitude and longitude of 0, setting to null');
|
||||||
|
latitude = null;
|
||||||
|
longitude = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: ReverseGeocodeResult = { country: null, state: null, city: null };
|
||||||
|
if (reverseGeocoding.enabled && longitude && latitude) {
|
||||||
|
result = await this.mapRepository.reverseGeocode({ latitude, longitude });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...result, latitude, longitude };
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAutoStackId(tags: ImmichTags | null): string | null {
|
private getAutoStackId(tags: ImmichTags | null): string | null {
|
||||||
@ -645,28 +668,6 @@ export class MetadataService {
|
|||||||
return tags.BurstID ?? tags.BurstUUID ?? tags.CameraBurstID ?? tags.MediaUniqueID ?? null;
|
return tags.BurstID ?? tags.BurstUUID ?? tags.CameraBurstID ?? tags.MediaUniqueID ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
|
|
||||||
return this.getDateTimeOriginalWithRawValue(tags).exifDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDateTimeOriginalWithRawValue(tags: ImmichTags | Tags | null): { exifDate: Date | null; rawValue: string } {
|
|
||||||
if (!tags) {
|
|
||||||
return { exifDate: null, rawValue: '' };
|
|
||||||
}
|
|
||||||
const first = firstDateTime(tags as Tags, EXIF_DATE_TAGS);
|
|
||||||
return { exifDate: exifDate(first), rawValue: first?.rawValue ?? '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTimeZone(exifTags: ImmichTags, rawValue: string) {
|
|
||||||
const timeZone = exifTags.tz ?? null;
|
|
||||||
if (timeZone == null && rawValue.endsWith('+00:00')) {
|
|
||||||
// exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly
|
|
||||||
// https://github.com/photostructure/exiftool-vendored.js/issues/203
|
|
||||||
return 'UTC+0';
|
|
||||||
}
|
|
||||||
return timeZone;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getBitsPerSample(tags: ImmichTags): number | null {
|
private getBitsPerSample(tags: ImmichTags): number | null {
|
||||||
const bitDepthTags = [
|
const bitDepthTags = [
|
||||||
tags.BitsPerSample,
|
tags.BitsPerSample,
|
||||||
@ -685,33 +686,37 @@ export class MetadataService {
|
|||||||
return bitsPerSample;
|
return bitsPerSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyVideoMetadata(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
private async getVideoTags(originalPath: string) {
|
||||||
const { videoStreams, format } = await this.mediaRepository.probe(asset.originalPath);
|
const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
|
||||||
|
|
||||||
|
const tags: Pick<ImmichTags, 'Duration' | 'Orientation'> = {};
|
||||||
|
|
||||||
if (videoStreams[0]) {
|
if (videoStreams[0]) {
|
||||||
switch (videoStreams[0].rotation) {
|
switch (videoStreams[0].rotation) {
|
||||||
case -90: {
|
case -90: {
|
||||||
exifData.orientation = Orientation.Rotate90CW;
|
tags.Orientation = Orientation.Rotate90CW;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 0: {
|
case 0: {
|
||||||
exifData.orientation = Orientation.Horizontal;
|
tags.Orientation = Orientation.Horizontal;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 90: {
|
case 90: {
|
||||||
exifData.orientation = Orientation.Rotate270CW;
|
tags.Orientation = Orientation.Rotate270CW;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 180: {
|
case 180: {
|
||||||
exifData.orientation = Orientation.Rotate180;
|
tags.Orientation = Orientation.Rotate180;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format.duration) {
|
if (format.duration) {
|
||||||
asset.duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS');
|
tags.Duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processSidecar(id: string, isSync: boolean): Promise<JobStatus> {
|
private async processSidecar(id: string, isSync: boolean): Promise<JobStatus> {
|
||||||
|
@ -6,6 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
|||||||
import { AssetFileType, UserMetadataKey } from 'src/enum';
|
import { AssetFileType, UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
||||||
@ -17,6 +18,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
|||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock';
|
import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock';
|
||||||
@ -64,6 +66,7 @@ const configs = {
|
|||||||
describe(NotificationService.name, () => {
|
describe(NotificationService.name, () => {
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let notificationMock: Mocked<INotificationRepository>;
|
let notificationMock: Mocked<INotificationRepository>;
|
||||||
@ -74,13 +77,23 @@ describe(NotificationService.name, () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
albumMock = newAlbumRepositoryMock();
|
albumMock = newAlbumRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
eventMock = newEventRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
notificationMock = newNotificationRepositoryMock();
|
notificationMock = newNotificationRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
|
|
||||||
sut = new NotificationService(systemMock, notificationMock, userMock, jobMock, loggerMock, assetMock, albumMock);
|
sut = new NotificationService(
|
||||||
|
eventMock,
|
||||||
|
systemMock,
|
||||||
|
notificationMock,
|
||||||
|
userMock,
|
||||||
|
jobMock,
|
||||||
|
loggerMock,
|
||||||
|
assetMock,
|
||||||
|
albumMock,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -6,7 +6,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
|||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.interface';
|
import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IEmailJob,
|
IEmailJob,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
@ -30,6 +30,7 @@ export class NotificationService {
|
|||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@ -74,6 +75,12 @@ export class NotificationService {
|
|||||||
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
|
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEmit({ event: 'session.delete' })
|
||||||
|
onSessionDelete({ sessionId }: ArgOf<'session.delete'>) {
|
||||||
|
// after the response is sent
|
||||||
|
setTimeout(() => this.eventRepository.clientSend(ClientEvent.SESSION_DELETE, sessionId, sessionId), 500);
|
||||||
|
}
|
||||||
|
|
||||||
async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {
|
async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {
|
||||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -1,19 +1,29 @@
|
|||||||
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { StorageService } from 'src/services/storage.service';
|
import { StorageService } from 'src/services/storage.service';
|
||||||
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||||
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
describe(StorageService.name, () => {
|
describe(StorageService.name, () => {
|
||||||
let sut: StorageService;
|
let sut: StorageService;
|
||||||
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
sut = new StorageService(storageMock, loggerMock);
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
|
|
||||||
|
sut = new StorageService(databaseMock, storageMock, loggerMock, systemMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -21,9 +31,35 @@ describe(StorageService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('onBootstrap', () => {
|
describe('onBootstrap', () => {
|
||||||
it('should create the library folder on initialization', () => {
|
it('should enable mount folder checking', async () => {
|
||||||
sut.onBootstrap();
|
systemMock.get.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.SYSTEM_FLAGS, { mountFiles: true });
|
||||||
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/encoded-video');
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library');
|
||||||
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile');
|
||||||
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if .immich is missing', async () => {
|
||||||
|
systemMock.get.mockResolvedValue({ mountFiles: true });
|
||||||
|
storageMock.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||||
|
|
||||||
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
|
||||||
|
|
||||||
|
expect(storageMock.writeFile).not.toHaveBeenCalled();
|
||||||
|
expect(systemMock.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if .immich is present but read-only', async () => {
|
||||||
|
systemMock.get.mockResolvedValue({ mountFiles: true });
|
||||||
|
storageMock.writeFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||||
|
|
||||||
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
|
||||||
|
|
||||||
|
expect(systemMock.set).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,23 +1,52 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { ImmichStartupError } from 'src/utils/events';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageService {
|
export class StorageService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
@Inject(ISystemMetadataRepository) private systemMetadata: ISystemMetadataRepository,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(StorageService.name);
|
this.logger.setContext(StorageService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEmit({ event: 'app.bootstrap' })
|
@OnEmit({ event: 'app.bootstrap' })
|
||||||
onBootstrap() {
|
async onBootstrap() {
|
||||||
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
|
await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => {
|
||||||
this.storageRepository.mkdirSync(libraryBase);
|
const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false };
|
||||||
|
|
||||||
|
this.logger.log('Verifying system mount folder checks');
|
||||||
|
|
||||||
|
// check each folder exists and is writable
|
||||||
|
for (const folder of Object.values(StorageFolder)) {
|
||||||
|
if (!flags.mountFiles) {
|
||||||
|
this.logger.log(`Writing initial mount file for the ${folder} folder`);
|
||||||
|
await this.verifyWriteAccess(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.verifyReadAccess(folder);
|
||||||
|
await this.verifyWriteAccess(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.mountFiles) {
|
||||||
|
flags.mountFiles = true;
|
||||||
|
await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
|
||||||
|
this.logger.log('Successfully enabled system mount folders checks');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('Successfully verified system mount folder checks');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteFiles(job: IDeleteFilesJob) {
|
async handleDeleteFiles(job: IDeleteFilesJob) {
|
||||||
@ -38,4 +67,38 @@ export class StorageService {
|
|||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async verifyReadAccess(folder: StorageFolder) {
|
||||||
|
const { filePath } = this.getMountFilePaths(folder);
|
||||||
|
try {
|
||||||
|
await this.storageRepository.readFile(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to read ${filePath}: ${error}`);
|
||||||
|
this.logger.error(
|
||||||
|
`The "${folder}" folder appears to be offline/missing, please make sure the volume is mounted with the correct permissions`,
|
||||||
|
);
|
||||||
|
throw new ImmichStartupError(`Failed to validate folder mount (read from "<MEDIA_LOCATION>/${folder}")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyWriteAccess(folder: StorageFolder) {
|
||||||
|
const { folderPath, filePath } = this.getMountFilePaths(folder);
|
||||||
|
try {
|
||||||
|
this.storageRepository.mkdirSync(folderPath);
|
||||||
|
await this.storageRepository.writeFile(filePath, Buffer.from(`${Date.now()}`));
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to write ${filePath}: ${error}`);
|
||||||
|
this.logger.error(
|
||||||
|
`The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`,
|
||||||
|
);
|
||||||
|
throw new ImmichStartupError(`Failed to validate folder mount (write to "<MEDIA_LOCATION>/${folder}")`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMountFilePaths(folder: StorageFolder) {
|
||||||
|
const folderPath = StorageCore.getBaseFolder(folder);
|
||||||
|
const filePath = join(folderPath, '.immich');
|
||||||
|
|
||||||
|
return { folderPath, filePath };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,6 +336,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
'{{y}}/{{MM}}-{{dd}}/{{filename}}',
|
'{{y}}/{{MM}}-{{dd}}/{{filename}}',
|
||||||
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
'{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
|
||||||
'{{y}}/{{MM}}/{{filename}}',
|
'{{y}}/{{MM}}/{{filename}}',
|
||||||
|
'{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
|
||||||
'{{y}}/{{MMM}}/{{filename}}',
|
'{{y}}/{{MMM}}/{{filename}}',
|
||||||
'{{y}}/{{MMMM}}/{{filename}}',
|
'{{y}}/{{MMMM}}/{{filename}}',
|
||||||
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
'{{y}}/{{MM}}/{{dd}}/{{filename}}',
|
||||||
|
@ -12,6 +12,9 @@ type Item<T extends EmitEvent> = {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class ImmichStartupError extends Error {}
|
||||||
|
export const isStartUpError = (error: unknown): error is ImmichStartupError => error instanceof ImmichStartupError;
|
||||||
|
|
||||||
export const setupEventHandlers = (moduleRef: ModuleRef) => {
|
export const setupEventHandlers = (moduleRef: ModuleRef) => {
|
||||||
const reflector = moduleRef.get(Reflector, { strict: false });
|
const reflector = moduleRef.get(Reflector, { strict: false });
|
||||||
const repository = moduleRef.get<IEventRepository>(IEventRepository);
|
const repository = moduleRef.get<IEventRepository>(IEventRepository);
|
||||||
|
@ -115,6 +115,7 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
// explicitly selects the video stream instead of leaving it up to FFmpeg
|
// explicitly selects the video stream instead of leaving it up to FFmpeg
|
||||||
`-map 0:${videoStream.index}`,
|
`-map 0:${videoStream.index}`,
|
||||||
|
'-strict unofficial',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (audioStream) {
|
if (audioStream) {
|
||||||
|
@ -9,6 +9,7 @@ import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/
|
|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||||
import { ApiService } from 'src/services/api.service';
|
import { ApiService } from 'src/services/api.service';
|
||||||
|
import { isStartUpError } from 'src/utils/events';
|
||||||
import { otelStart } from 'src/utils/instrumentation';
|
import { otelStart } from 'src/utils/instrumentation';
|
||||||
import { useSwagger } from 'src/utils/misc';
|
import { useSwagger } from 'src/utils/misc';
|
||||||
|
|
||||||
@ -73,6 +74,9 @@ async function bootstrap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bootstrap().catch((error) => {
|
bootstrap().catch((error) => {
|
||||||
|
if (!isStartUpError(error)) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw error;
|
}
|
||||||
|
// eslint-disable-next-line unicorn/no-process-exit
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ import { MicroservicesModule } from 'src/app.module';
|
|||||||
import { envName, serverVersion } from 'src/constants';
|
import { envName, serverVersion } from 'src/constants';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||||
|
import { isStartUpError } from 'src/utils/events';
|
||||||
import { otelStart } from 'src/utils/instrumentation';
|
import { otelStart } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
@ -25,7 +26,9 @@ export async function bootstrap() {
|
|||||||
|
|
||||||
if (!isMainThread) {
|
if (!isMainThread) {
|
||||||
bootstrap().catch((error) => {
|
bootstrap().catch((error) => {
|
||||||
|
if (!isStartUpError(error)) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
}
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
12
web/package-lock.json
generated
12
web/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.113.1",
|
"version": "1.114.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.113.1",
|
"version": "1.114.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",
|
||||||
@ -17,7 +17,6 @@
|
|||||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||||
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||||
"@zoom-image/svelte": "^0.2.6",
|
"@zoom-image/svelte": "^0.2.6",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
|
||||||
"dom-to-image": "^2.6.0",
|
"dom-to-image": "^2.6.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"intl-messageformat": "^10.5.14",
|
"intl-messageformat": "^10.5.14",
|
||||||
@ -75,7 +74,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.113.1",
|
"version": "1.114.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"
|
||||||
@ -3284,11 +3283,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/copy-image-clipboard": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA=="
|
|
||||||
},
|
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.37.1",
|
"version": "3.37.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.113.1",
|
"version": "1.114.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",
|
||||||
@ -73,7 +73,6 @@
|
|||||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
||||||
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
||||||
"@zoom-image/svelte": "^0.2.6",
|
"@zoom-image/svelte": "^0.2.6",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
|
||||||
"dom-to-image": "^2.6.0",
|
"dom-to-image": "^2.6.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"intl-messageformat": "^10.5.14",
|
"intl-messageformat": "^10.5.14",
|
||||||
|
@ -19,7 +19,7 @@ describe('AlbumCover component', () => {
|
|||||||
const img = component.getByTestId('album-image') as HTMLImageElement;
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
expect(img.alt).toBe('someName');
|
expect(img.alt).toBe('someName');
|
||||||
expect(img.getAttribute('loading')).toBe('lazy');
|
expect(img.getAttribute('loading')).toBe('lazy');
|
||||||
expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text');
|
expect(img.className).toBe('size-full rounded-xl object-cover aspect-square text');
|
||||||
expect(img.getAttribute('src')).toBe('/asdf');
|
expect(img.getAttribute('src')).toBe('/asdf');
|
||||||
expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' });
|
expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' });
|
||||||
});
|
});
|
||||||
@ -36,7 +36,7 @@ describe('AlbumCover component', () => {
|
|||||||
const img = component.getByTestId('album-image') as HTMLImageElement;
|
const img = component.getByTestId('album-image') as HTMLImageElement;
|
||||||
expect(img.alt).toBe('unnamed_album');
|
expect(img.alt).toBe('unnamed_album');
|
||||||
expect(img.getAttribute('loading')).toBe('eager');
|
expect(img.getAttribute('loading')).toBe('eager');
|
||||||
expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf');
|
expect(img.className).toBe('size-full rounded-xl object-cover aspect-square asdf');
|
||||||
expect(img.getAttribute('src')).toStrictEqual(expect.any(String));
|
expect(img.getAttribute('src')).toStrictEqual(expect.any(String));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<AlbumCover {album} {preload} class="h-full w-full transition-all duration-300 hover:shadow-lg" />
|
<AlbumCover {album} {preload} class="transition-all duration-300 hover:shadow-lg" />
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<p
|
<p
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
mdiPresentationPlay,
|
mdiPresentationPlay,
|
||||||
mdiUpload,
|
mdiUpload,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { canCopyImagesToClipboard } from 'copy-image-clipboard';
|
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
@ -101,7 +101,7 @@
|
|||||||
on:click={onZoomImage}
|
on:click={onZoomImage}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if canCopyImagesToClipboard() && asset.type === AssetTypeEnum.Image}
|
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image}
|
||||||
<CircleIconButton color="opaque" icon={mdiContentCopy} title={$t('copy_image')} on:click={onCopyImage} />
|
<CircleIconButton color="opaque" icon={mdiContentCopy} title={$t('copy_image')} on:click={onCopyImage} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -8,17 +8,17 @@
|
|||||||
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
import { isWebCompatibleImage, canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard';
|
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
|
import { type SwipeCustomEvent, swipe } from 'svelte-gestures';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let preloadAssets: AssetResponseDto[] | undefined = undefined;
|
export let preloadAssets: AssetResponseDto[] | undefined = undefined;
|
||||||
@ -81,23 +81,19 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
copyImage = async () => {
|
copyImage = async () => {
|
||||||
if (!canCopyImagesToClipboard()) {
|
if (!canCopyImageToClipboard()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await copyImageToClipboard(assetFileUrl);
|
await copyImageToClipboard($photoViewer ?? assetFileUrl);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: $t('copied_image_to_clipboard'),
|
message: $t('copied_image_to_clipboard'),
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error [photo-viewer]:', error);
|
handleError(error, $t('copy_error'));
|
||||||
notificationController.show({
|
|
||||||
type: NotificationType.Error,
|
|
||||||
message: 'Copying image to clipboard failed.',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -152,7 +148,7 @@
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{#if imageError}
|
{#if imageError}
|
||||||
<BrokenAsset square />
|
<BrokenAsset class="text-xl" />
|
||||||
{/if}
|
{/if}
|
||||||
<!-- svelte-ignore a11y-missing-attribute -->
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
<img bind:this={loader} style="display:none" src={imageLoaderUrl} aria-hidden="true" />
|
<img bind:this={loader} style="display:none" src={imageLoaderUrl} aria-hidden="true" />
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user