From 079aa13edbc5edc4055466d88abdb26c8f265e98 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 14 Aug 2023 21:22:33 -0400 Subject: [PATCH 01/14] fix(web): use server api media types (#3687) --- web/src/lib/utils/file-uploader.ts | 62 +++++++----------------------- 1 file changed, 13 insertions(+), 49 deletions(-) diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index fad841c221..533ab372ef 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -1,62 +1,25 @@ import { uploadAssetsStore } from '$lib/stores/upload'; import { addAssetsToAlbum } from '$lib/utils/asset-utils'; -import type { AssetFileUploadResponseDto } from '@api'; +import { api, AssetFileUploadResponseDto } from '@api'; import axios from 'axios'; import { notificationController, NotificationType } from './../components/shared-components/notification/notification'; -const extensions = [ - '.3fr', - '.3gp', - '.ari', - '.arw', - '.avi', - '.avif', - '.cap', - '.cin', - '.cr2', - '.cr3', - '.crw', - '.dcr', - '.dng', - '.erf', - '.fff', - '.flv', - '.gif', - '.heic', - '.heif', - '.iiq', - '.jpeg', - '.jpg', - '.k25', - '.kdc', - '.mkv', - '.mov', - '.mp2t', - '.mp4', - '.mpeg', - '.mrw', - '.nef', - '.orf', - '.ori', - '.pef', - '.png', - '.raf', - '.raw', - '.rwl', - '.sr2', - '.srf', - '.srw', - '.tiff', - '.webm', - '.webp', - '.wmv', - '.x3f', -]; +let _extensions: string[]; + +const getExtensions = async () => { + if (!_extensions) { + const { data } = await api.serverInfoApi.getSupportedMediaTypes(); + _extensions = [...data.image, ...data.video]; + } + return _extensions; +}; export const openFileUploadDialog = async ( albumId: string | undefined = undefined, sharedKey: string | undefined = undefined, ) => { + const extensions = await getExtensions(); + return new Promise<(string | undefined)[]>((resolve, reject) => { try { const fileSelector = document.createElement('input'); @@ -87,6 +50,7 @@ export const fileUploadHandler = async ( albumId: string | undefined = undefined, sharedKey: string | undefined = undefined, ) => { + const extensions = await getExtensions(); const iterable = { files: files.filter((file) => extensions.some((ext) => file.name.toLowerCase().endsWith(ext)))[Symbol.iterator](), From f1b8a7ab5474fa3364fe3a5835374b85232d1dd0 Mon Sep 17 00:00:00 2001 From: Russell Tan Date: Mon, 14 Aug 2023 18:37:17 -0700 Subject: [PATCH 02/14] fix(server): Does not assign lat/lon if they are at 0,0 #2991 (#3669) * fix(server): Does not assign lat/lon if they are at 0,0 #2991 * Adds migration file to fix null island rows * Removed down migration * Leave empty down function --- .../migrations/1692057328660-fixGPSNullIsland.ts | 13 +++++++++++++ .../processors/metadata-extraction.processor.ts | 12 ++++++++++-- .../microservices/utils/exif/coordinates.spec.ts | 12 ++++++++++++ server/src/microservices/utils/exif/coordinates.ts | 11 +++++++++-- 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts diff --git a/server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts b/server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts new file mode 100644 index 0000000000..74dc40a474 --- /dev/null +++ b/server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class FixGPSNullIsland1692057328660 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`UPDATE "exif" SET latitude = NULL, longitude = NULL WHERE latitude = 0 AND longitude = 0;`); + } + + public async down(): Promise { + // Setting lat,lon to 0 not necessary + } + +} diff --git a/server/src/microservices/processors/metadata-extraction.processor.ts b/server/src/microservices/processors/metadata-extraction.processor.ts index a37ee6a23a..a5f3ed4dd3 100644 --- a/server/src/microservices/processors/metadata-extraction.processor.ts +++ b/server/src/microservices/processors/metadata-extraction.processor.ts @@ -308,8 +308,16 @@ export class MetadataExtractionProcessor { const latitude = getExifProperty('GPSLatitude'); const longitude = getExifProperty('GPSLongitude'); - newExif.latitude = latitude !== null ? parseLatitude(latitude) : null; - newExif.longitude = longitude !== null ? parseLongitude(longitude) : null; + const lat = parseLatitude(latitude); + const lon = parseLongitude(longitude); + + if (lat === 0 && lon === 0) { + this.logger.warn(`Latitude & Longitude were on Null Island (${lat},${lon}), not assigning coordinates`); + } else { + newExif.latitude = lat; + newExif.longitude = lon; + } + if (getExifProperty('MotionPhoto')) { // Seen on more recent Pixel phones: starting as early as Pixel 4a, possibly earlier. const rawDirectory = getExifProperty('Directory'); diff --git a/server/src/microservices/utils/exif/coordinates.spec.ts b/server/src/microservices/utils/exif/coordinates.spec.ts index 975d1e9969..223a7671ec 100644 --- a/server/src/microservices/utils/exif/coordinates.spec.ts +++ b/server/src/microservices/utils/exif/coordinates.spec.ts @@ -23,6 +23,12 @@ describe('parsing latitude from string input', () => { }); }); +describe('parsing latitude from null input', () => { + it('returns null for null input', () => { + expect(parseLatitude(null)).toBeNull(); + }); +}); + describe('parsing longitude from string input', () => { it('returns null for invalid inputs', () => { expect(parseLongitude('')).toBeNull(); @@ -44,3 +50,9 @@ describe('parsing longitude from string input', () => { expect(parseLongitude('-0.0')).toBeCloseTo(-0.0); }); }); + +describe('parsing longitude from null input', () => { + it('returns null for null input', () => { + expect(parseLongitude(null)).toBeNull(); + }); +}); diff --git a/server/src/microservices/utils/exif/coordinates.ts b/server/src/microservices/utils/exif/coordinates.ts index 3a07b0c173..03aeb17f09 100644 --- a/server/src/microservices/utils/exif/coordinates.ts +++ b/server/src/microservices/utils/exif/coordinates.ts @@ -1,6 +1,9 @@ import { isNumberInRange } from '../numbers'; -export function parseLatitude(input: string | number): number | null { +export function parseLatitude(input: string | number | null): number | null { + if (input === null) { + return null; + } const latitude = typeof input === 'string' ? Number.parseFloat(input) : input; if (isNumberInRange(latitude, -90, 90)) { @@ -9,7 +12,11 @@ export function parseLatitude(input: string | number): number | null { return null; } -export function parseLongitude(input: string | number): number | null { +export function parseLongitude(input: string | number | null): number | null { + if (input === null) { + return null; + } + const longitude = typeof input === 'string' ? Number.parseFloat(input) : input; if (isNumberInRange(longitude, -180, 180)) { From 7ca6f80ed26aa78ed21a3e1cf3fff02f2223a594 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 14 Aug 2023 23:14:52 -0400 Subject: [PATCH 03/14] fix(server): display insta-360 (#3688) --- server/src/domain/domain.constant.ts | 2 ++ web/src/lib/components/asset-viewer/asset-viewer.svelte | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index a1cb0a7dfe..b06720613c 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -48,6 +48,7 @@ const image: Record = { '.heic': ['image/heic'], '.heif': ['image/heif'], '.iiq': ['image/iiq', 'image/x-phaseone-iiq'], + '.insp': ['image/jpeg'], '.jpeg': ['image/jpeg'], '.jpg': ['image/jpeg'], '.jxl': ['image/jxl'], @@ -79,6 +80,7 @@ const video: Record = { '.3gp': ['video/3gpp'], '.avi': ['video/avi', 'video/msvideo', 'video/vnd.avi', 'video/x-msvideo'], '.flv': ['video/x-flv'], + '.insv': ['video/mp4'], '.m2ts': ['video/mp2t'], '.mkv': ['video/x-matroska'], '.mov': ['video/quicktime'], diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index e65b45e808..5df79b1590 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -297,7 +297,9 @@ on:close={closeViewer} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} /> - {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR} + {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || asset.originalPath + .toLowerCase() + .endsWith('.insp')} {:else} From a3b6095b614ae5123cf7ce3fbead8c234da7d56e Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Tue, 15 Aug 2023 12:45:48 +0000 Subject: [PATCH 04/14] Version v1.73.0 --- cli/src/api/open-api/api.ts | 2 +- cli/src/api/open-api/base.ts | 2 +- cli/src/api/open-api/common.ts | 2 +- cli/src/api/open-api/configuration.ts | 2 +- cli/src/api/open-api/index.ts | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- server/immich-openapi-specs.json | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/src/api/open-api/api.ts | 2 +- web/src/api/open-api/base.ts | 2 +- web/src/api/open-api/common.ts | 2 +- web/src/api/open-api/configuration.ts | 2 +- web/src/api/open-api/index.ts | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index e8662175a4..956f19ba7b 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/base.ts b/cli/src/api/open-api/base.ts index d636bfdd9a..7109cba71e 100644 --- a/cli/src/api/open-api/base.ts +++ b/cli/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/common.ts b/cli/src/api/open-api/common.ts index 6b75041a70..66890914ad 100644 --- a/cli/src/api/open-api/common.ts +++ b/cli/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/configuration.ts b/cli/src/api/open-api/configuration.ts index 275f99f72a..21fd71538b 100644 --- a/cli/src/api/open-api/configuration.ts +++ b/cli/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/index.ts b/cli/src/api/open-api/index.ts index 7047458436..937e9a24ad 100644 --- a/cli/src/api/open-api/index.ts +++ b/cli/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 4547dcb052..b2fa1b28e3 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.72.2" +version = "1.73.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 0de6cb8aa7..e9be5f6548 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 95, - "android.injected.version.name" => "1.72.2", + "android.injected.version.code" => 96, + "android.injected.version.name" => "1.73.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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 56bf4ba34e..7707eadb9d 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.72.2" + version_number: "1.73.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2942d1d854..1e20582fae 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.72.2 +- API version: 1.73.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 5c75bfb7e9..dc90a5373c 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.72.2+95 +version: 1.73.0+96 isar_version: &isar_version 3.1.0+1 environment: diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index a60fcbd544..59543353fb 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -4590,7 +4590,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.72.2", + "version": "1.73.0", "contact": {} }, "tags": [], diff --git a/server/package-lock.json b/server/package-lock.json index fa3fbf12a6..e6e2de6aba 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.72.2", + "version": "1.73.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.72.2", + "version": "1.73.0", "license": "UNLICENSED", "dependencies": { "@babel/runtime": "^7.20.13", diff --git a/server/package.json b/server/package.json index 2baf087bf6..cdfb87d59a 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.72.2", + "version": "1.73.0", "description": "", "author": "", "private": true, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index e8662175a4..956f19ba7b 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index d636bfdd9a..7109cba71e 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index 6b75041a70..66890914ad 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index 275f99f72a..21fd71538b 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index 7047458436..937e9a24ad 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.72.2 + * The version of the OpenAPI document: 1.73.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). From a75f368d5b323d978e9a83a9723495bccf1d8402 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Aug 2023 09:42:28 -0500 Subject: [PATCH 05/14] chore: post update --- mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Podfile.lock | 2 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/report.xml | 12 ++++++------ 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 45b5cef2d8..d268a40aee 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 7b7cbd0d57..4902388c6d 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -163,4 +163,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 9f463abbe7..8bb1e31927 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -379,7 +379,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -515,7 +515,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 041dcd2ebd..ae8c18518e 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -59,11 +59,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.70.0 + 1.73.0 CFBundleSignature ???? CFBundleVersion - 110 + 113 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index f1ee5f8278..3f00787c15 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + From efc7fdb669002ab8ed6363141e1ea0400cd2355b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 15 Aug 2023 11:49:32 -0400 Subject: [PATCH 06/14] fix(web,server): use POST request to get download info (#3694) * fix(web,server): use POST request to get download info * chore: open api --- cli/src/api/open-api/api.ts | 107 ++++++------ mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | 5 +- mobile/openapi/doc/AssetApi.md | 20 +-- mobile/openapi/doc/DownloadInfoDto.md | 18 +++ mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api/asset_api.dart | 48 ++---- mobile/openapi/lib/api_client.dart | 2 + .../openapi/lib/model/download_info_dto.dart | 150 +++++++++++++++++ mobile/openapi/test/asset_api_test.dart | 2 +- .../openapi/test/download_info_dto_test.dart | 42 +++++ server/immich-openapi-specs.json | 153 +++++++++--------- server/src/domain/asset/asset.service.ts | 6 +- server/src/domain/asset/dto/download.dto.ts | 3 +- .../immich/controllers/asset.controller.ts | 8 +- web/src/api/open-api/api.ts | 107 ++++++------ web/src/lib/utils/asset-utils.ts | 10 +- 17 files changed, 426 insertions(+), 259 deletions(-) create mode 100644 mobile/openapi/doc/DownloadInfoDto.md create mode 100644 mobile/openapi/lib/model/download_info_dto.dart create mode 100644 mobile/openapi/test/download_info_dto_test.dart diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 956f19ba7b..8ade1d5fde 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -1132,6 +1132,37 @@ export interface DownloadArchiveInfo { */ 'size': number; } +/** + * + * @export + * @interface DownloadInfoDto + */ +export interface DownloadInfoDto { + /** + * + * @type {string} + * @memberof DownloadInfoDto + */ + 'albumId'?: string; + /** + * + * @type {number} + * @memberof DownloadInfoDto + */ + 'archiveSize'?: number; + /** + * + * @type {Array} + * @memberof DownloadInfoDto + */ + 'assetIds'?: Array; + /** + * + * @type {string} + * @memberof DownloadInfoDto + */ + 'userId'?: string; +} /** * * @export @@ -4880,7 +4911,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration downloadArchive: async (assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'assetIdsDto' is not null or undefined assertParamExists('downloadArchive', 'assetIdsDto', assetIdsDto) - const localVarPath = `/asset/download`; + const localVarPath = `/asset/download/archive`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5379,16 +5410,15 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * - * @param {Array} [assetIds] - * @param {string} [albumId] - * @param {string} [userId] - * @param {number} [archiveSize] + * @param {DownloadInfoDto} downloadInfoDto * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getDownloadInfo: async (assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/asset/download`; + getDownloadInfo: async (downloadInfoDto: DownloadInfoDto, key?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'downloadInfoDto' is not null or undefined + assertParamExists('getDownloadInfo', 'downloadInfoDto', downloadInfoDto) + const localVarPath = `/asset/download/info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5396,7 +5426,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -5409,31 +5439,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (assetIds) { - localVarQueryParameter['assetIds'] = assetIds; - } - - if (albumId !== undefined) { - localVarQueryParameter['albumId'] = albumId; - } - - if (userId !== undefined) { - localVarQueryParameter['userId'] = userId; - } - - if (archiveSize !== undefined) { - localVarQueryParameter['archiveSize'] = archiveSize; - } - if (key !== undefined) { localVarQueryParameter['key'] = key; } + localVarHeaderParameter['Content-Type'] = 'application/json'; + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(downloadInfoDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -6141,16 +6158,13 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * - * @param {Array} [assetIds] - * @param {string} [albumId] - * @param {string} [userId] - * @param {number} [archiveSize] + * @param {DownloadInfoDto} downloadInfoDto * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getDownloadInfo(assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(assetIds, albumId, userId, archiveSize, key, options); + async getDownloadInfo(downloadInfoDto: DownloadInfoDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(downloadInfoDto, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6406,8 +6420,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(axios, basePath)); + getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -6788,31 +6802,10 @@ export interface AssetApiGetByTimeBucketRequest { export interface AssetApiGetDownloadInfoRequest { /** * - * @type {Array} + * @type {DownloadInfoDto} * @memberof AssetApiGetDownloadInfo */ - readonly assetIds?: Array - - /** - * - * @type {string} - * @memberof AssetApiGetDownloadInfo - */ - readonly albumId?: string - - /** - * - * @type {string} - * @memberof AssetApiGetDownloadInfo - */ - readonly userId?: string - - /** - * - * @type {number} - * @memberof AssetApiGetDownloadInfo - */ - readonly archiveSize?: number + readonly downloadInfoDto: DownloadInfoDto /** * @@ -7281,8 +7274,8 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index ea0993be8c..bf485ef08b 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -45,6 +45,7 @@ doc/DeleteAssetDto.md doc/DeleteAssetResponseDto.md doc/DeleteAssetStatus.md doc/DownloadArchiveInfo.md +doc/DownloadInfoDto.md doc/DownloadResponseDto.md doc/ExifResponseDto.md doc/ImportAssetDto.md @@ -186,6 +187,7 @@ lib/model/delete_asset_dto.dart lib/model/delete_asset_response_dto.dart lib/model/delete_asset_status.dart lib/model/download_archive_info.dart +lib/model/download_info_dto.dart lib/model/download_response_dto.dart lib/model/exif_response_dto.dart lib/model/import_asset_dto.dart @@ -298,6 +300,7 @@ test/delete_asset_dto_test.dart test/delete_asset_response_dto_test.dart test/delete_asset_status_test.dart test/download_archive_info_test.dart +test/download_info_dto_test.dart test/download_response_dto_test.dart test/exif_response_dto_test.dart test/import_asset_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 1e20582fae..9167613b25 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -91,7 +91,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | *AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset | -*AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download | +*AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download/archive | *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | *AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | @@ -101,7 +101,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**getByTimeBucket**](doc//AssetApi.md#getbytimebucket) | **GET** /asset/time-bucket | *AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | *AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | -*AssetApi* | [**getDownloadInfo**](doc//AssetApi.md#getdownloadinfo) | **GET** /asset/download | +*AssetApi* | [**getDownloadInfo**](doc//AssetApi.md#getdownloadinfo) | **POST** /asset/download/info | *AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | *AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | *AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | @@ -216,6 +216,7 @@ Class | Method | HTTP request | Description - [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md) - [DeleteAssetStatus](doc//DeleteAssetStatus.md) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) + - [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadResponseDto](doc//DownloadResponseDto.md) - [ExifResponseDto](doc//ExifResponseDto.md) - [ImportAssetDto](doc//ImportAssetDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 2999f2de2a..5c1c7bb4bf 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -13,7 +13,7 @@ Method | HTTP request | Description [**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check | [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | [**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset | -[**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download | +[**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download/archive | [**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | @@ -23,7 +23,7 @@ Method | HTTP request | Description [**getByTimeBucket**](AssetApi.md#getbytimebucket) | **GET** /asset/time-bucket | [**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations | [**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects | -[**getDownloadInfo**](AssetApi.md#getdownloadinfo) | **GET** /asset/download | +[**getDownloadInfo**](AssetApi.md#getdownloadinfo) | **POST** /asset/download/info | [**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker | [**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane | [**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | @@ -842,7 +842,7 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getDownloadInfo** -> DownloadResponseDto getDownloadInfo(assetIds, albumId, userId, archiveSize, key) +> DownloadResponseDto getDownloadInfo(downloadInfoDto, key) @@ -865,14 +865,11 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = AssetApi(); -final assetIds = []; // List | -final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | -final archiveSize = 8.14; // num | +final downloadInfoDto = DownloadInfoDto(); // DownloadInfoDto | final key = key_example; // String | try { - final result = api_instance.getDownloadInfo(assetIds, albumId, userId, archiveSize, key); + final result = api_instance.getDownloadInfo(downloadInfoDto, key); print(result); } catch (e) { print('Exception when calling AssetApi->getDownloadInfo: $e\n'); @@ -883,10 +880,7 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **assetIds** | [**List**](String.md)| | [optional] [default to const []] - **albumId** | **String**| | [optional] - **userId** | **String**| | [optional] - **archiveSize** | **num**| | [optional] + **downloadInfoDto** | [**DownloadInfoDto**](DownloadInfoDto.md)| | **key** | **String**| | [optional] ### Return type @@ -899,7 +893,7 @@ Name | Type | Description | Notes ### HTTP request headers - - **Content-Type**: Not defined + - **Content-Type**: application/json - **Accept**: application/json [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/DownloadInfoDto.md b/mobile/openapi/doc/DownloadInfoDto.md new file mode 100644 index 0000000000..14d5d1e712 --- /dev/null +++ b/mobile/openapi/doc/DownloadInfoDto.md @@ -0,0 +1,18 @@ +# openapi.model.DownloadInfoDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**albumId** | **String** | | [optional] +**archiveSize** | **int** | | [optional] +**assetIds** | **List** | | [optional] [default to const []] +**userId** | **String** | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 644244a103..a38d8784e3 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -81,6 +81,7 @@ part 'model/delete_asset_dto.dart'; part 'model/delete_asset_response_dto.dart'; part 'model/delete_asset_status.dart'; part 'model/download_archive_info.dart'; +part 'model/download_info_dto.dart'; part 'model/download_response_dto.dart'; part 'model/exif_response_dto.dart'; part 'model/import_asset_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 6f4c391bf8..ba9f1d5a1f 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -230,7 +230,7 @@ class AssetApi { return null; } - /// Performs an HTTP 'POST /asset/download' operation and returns the [Response]. + /// Performs an HTTP 'POST /asset/download/archive' operation and returns the [Response]. /// Parameters: /// /// * [AssetIdsDto] assetIdsDto (required): @@ -238,7 +238,7 @@ class AssetApi { /// * [String] key: Future downloadArchiveWithHttpInfo(AssetIdsDto assetIdsDto, { String? key, }) async { // ignore: prefer_const_declarations - final path = r'/asset/download'; + final path = r'/asset/download/archive'; // ignore: prefer_final_locals Object? postBody = assetIdsDto; @@ -853,51 +853,33 @@ class AssetApi { return null; } - /// Performs an HTTP 'GET /asset/download' operation and returns the [Response]. + /// Performs an HTTP 'POST /asset/download/info' operation and returns the [Response]. /// Parameters: /// - /// * [List] assetIds: - /// - /// * [String] albumId: - /// - /// * [String] userId: - /// - /// * [num] archiveSize: + /// * [DownloadInfoDto] downloadInfoDto (required): /// /// * [String] key: - Future getDownloadInfoWithHttpInfo({ List? assetIds, String? albumId, String? userId, num? archiveSize, String? key, }) async { + Future getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, }) async { // ignore: prefer_const_declarations - final path = r'/asset/download'; + final path = r'/asset/download/info'; // ignore: prefer_final_locals - Object? postBody; + Object? postBody = downloadInfoDto; final queryParams = []; final headerParams = {}; final formParams = {}; - if (assetIds != null) { - queryParams.addAll(_queryParams('multi', 'assetIds', assetIds)); - } - if (albumId != null) { - queryParams.addAll(_queryParams('', 'albumId', albumId)); - } - if (userId != null) { - queryParams.addAll(_queryParams('', 'userId', userId)); - } - if (archiveSize != null) { - queryParams.addAll(_queryParams('', 'archiveSize', archiveSize)); - } if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } - const contentTypes = []; + const contentTypes = ['application/json']; return apiClient.invokeAPI( path, - 'GET', + 'POST', queryParams, postBody, headerParams, @@ -908,17 +890,11 @@ class AssetApi { /// Parameters: /// - /// * [List] assetIds: - /// - /// * [String] albumId: - /// - /// * [String] userId: - /// - /// * [num] archiveSize: + /// * [DownloadInfoDto] downloadInfoDto (required): /// /// * [String] key: - Future getDownloadInfo({ List? assetIds, String? albumId, String? userId, num? archiveSize, String? key, }) async { - final response = await getDownloadInfoWithHttpInfo( assetIds: assetIds, albumId: albumId, userId: userId, archiveSize: archiveSize, key: key, ); + Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, }) async { + final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index fd1252cc78..eb76c12c58 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -257,6 +257,8 @@ class ApiClient { return DeleteAssetStatusTypeTransformer().decode(value); case 'DownloadArchiveInfo': return DownloadArchiveInfo.fromJson(value); + case 'DownloadInfoDto': + return DownloadInfoDto.fromJson(value); case 'DownloadResponseDto': return DownloadResponseDto.fromJson(value); case 'ExifResponseDto': diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart new file mode 100644 index 0000000000..fa3a06eeac --- /dev/null +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -0,0 +1,150 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class DownloadInfoDto { + /// Returns a new [DownloadInfoDto] instance. + DownloadInfoDto({ + this.albumId, + this.archiveSize, + this.assetIds = const [], + this.userId, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? albumId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + int? archiveSize; + + List assetIds; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? userId; + + @override + bool operator ==(Object other) => identical(this, other) || other is DownloadInfoDto && + other.albumId == albumId && + other.archiveSize == archiveSize && + other.assetIds == assetIds && + other.userId == userId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumId == null ? 0 : albumId!.hashCode) + + (archiveSize == null ? 0 : archiveSize!.hashCode) + + (assetIds.hashCode) + + (userId == null ? 0 : userId!.hashCode); + + @override + String toString() => 'DownloadInfoDto[albumId=$albumId, archiveSize=$archiveSize, assetIds=$assetIds, userId=$userId]'; + + Map toJson() { + final json = {}; + if (this.albumId != null) { + json[r'albumId'] = this.albumId; + } else { + // json[r'albumId'] = null; + } + if (this.archiveSize != null) { + json[r'archiveSize'] = this.archiveSize; + } else { + // json[r'archiveSize'] = null; + } + json[r'assetIds'] = this.assetIds; + if (this.userId != null) { + json[r'userId'] = this.userId; + } else { + // json[r'userId'] = null; + } + return json; + } + + /// Returns a new [DownloadInfoDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DownloadInfoDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return DownloadInfoDto( + albumId: mapValueOfType(json, r'albumId'), + archiveSize: mapValueOfType(json, r'archiveSize'), + assetIds: json[r'assetIds'] is List + ? (json[r'assetIds'] as List).cast() + : const [], + userId: mapValueOfType(json, r'userId'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = DownloadInfoDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = DownloadInfoDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DownloadInfoDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = DownloadInfoDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 588902a14c..ebb472c4a0 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -97,7 +97,7 @@ void main() { // TODO }); - //Future getDownloadInfo({ List assetIds, String albumId, String userId, num archiveSize, String key }) async + //Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String key }) async test('test getDownloadInfo', () async { // TODO }); diff --git a/mobile/openapi/test/download_info_dto_test.dart b/mobile/openapi/test/download_info_dto_test.dart new file mode 100644 index 0000000000..5efd4e11eb --- /dev/null +++ b/mobile/openapi/test/download_info_dto_test.dart @@ -0,0 +1,42 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for DownloadInfoDto +void main() { + // final instance = DownloadInfoDto(); + + group('test DownloadInfoDto', () { + // String albumId + test('to test the property `albumId`', () async { + // TODO + }); + + // int archiveSize + test('to test the property `archiveSize`', () async { + // TODO + }); + + // List assetIds (default value: const []) + test('to test the property `assetIds`', () async { + // TODO + }); + + // String userId + test('to test the property `userId`', () async { + // TODO + }); + + + }); + +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 59543353fb..f193eb25e4 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1026,84 +1026,7 @@ ] } }, - "/asset/download": { - "get": { - "operationId": "getDownloadInfo", - "parameters": [ - { - "name": "assetIds", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "albumId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "userId", - "required": false, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "archiveSize", - "required": false, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - }, + "/asset/download/archive": { "post": { "operationId": "downloadArchive", "parameters": [ @@ -1155,6 +1078,57 @@ ] } }, + "/asset/download/info": { + "post": { + "operationId": "getDownloadInfo", + "parameters": [ + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadInfoDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DownloadResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Asset" + ] + } + }, "/asset/download/{id}": { "post": { "operationId": "downloadFile", @@ -5549,6 +5523,29 @@ ], "type": "object" }, + "DownloadInfoDto": { + "properties": { + "albumId": { + "format": "uuid", + "type": "string" + }, + "archiveSize": { + "type": "integer" + }, + "assetIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "userId": { + "format": "uuid", + "type": "string" + } + }, + "type": "object" + }, "DownloadResponseDto": { "properties": { "archives": { diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index fdd6df6efd..ac655b8077 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -13,7 +13,7 @@ import { IAssetRepository } from './asset.repository'; import { AssetIdsDto, DownloadArchiveInfo, - DownloadDto, + DownloadInfoDto, DownloadResponseDto, MemoryLaneDto, TimeBucketAssetDto, @@ -176,7 +176,7 @@ export class AssetService { return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath)); } - async getDownloadInfo(authUser: AuthUserDto, dto: DownloadDto): Promise { + async getDownloadInfo(authUser: AuthUserDto, dto: DownloadInfoDto): Promise { const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; const archives: DownloadArchiveInfo[] = []; let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; @@ -234,7 +234,7 @@ export class AssetService { return { stream: zip.stream }; } - private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadDto): Promise> { + private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadInfoDto): Promise> { const PAGINATION_SIZE = 2500; if (dto.assetIds) { diff --git a/server/src/domain/asset/dto/download.dto.ts b/server/src/domain/asset/dto/download.dto.ts index c2cf85685a..604a8ea5fb 100644 --- a/server/src/domain/asset/dto/download.dto.ts +++ b/server/src/domain/asset/dto/download.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsOptional, IsPositive } from 'class-validator'; import { ValidateUUID } from '../../domain.util'; -export class DownloadDto { +export class DownloadInfoDto { @ValidateUUID({ each: true, optional: true }) assetIds?: string[]; @@ -15,6 +15,7 @@ export class DownloadDto { @IsInt() @IsPositive() @IsOptional() + @ApiProperty({ type: 'integer' }) archiveSize?: number; } diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index ba3de02cc5..b55cbb870d 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -5,7 +5,7 @@ import { AssetStatsDto, AssetStatsResponseDto, AuthUserDto, - DownloadDto, + DownloadInfoDto, DownloadResponseDto, MapMarkerResponseDto, MemoryLaneDto, @@ -39,13 +39,13 @@ export class AssetController { } @SharedLinkRoute() - @Get('download') - getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Query() dto: DownloadDto): Promise { + @Post('download/info') + getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Body() dto: DownloadInfoDto): Promise { return this.service.getDownloadInfo(authUser, dto); } @SharedLinkRoute() - @Post('download') + @Post('download/archive') @HttpCode(HttpStatus.OK) @ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) downloadArchive(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetIdsDto): Promise { diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 956f19ba7b..8ade1d5fde 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1132,6 +1132,37 @@ export interface DownloadArchiveInfo { */ 'size': number; } +/** + * + * @export + * @interface DownloadInfoDto + */ +export interface DownloadInfoDto { + /** + * + * @type {string} + * @memberof DownloadInfoDto + */ + 'albumId'?: string; + /** + * + * @type {number} + * @memberof DownloadInfoDto + */ + 'archiveSize'?: number; + /** + * + * @type {Array} + * @memberof DownloadInfoDto + */ + 'assetIds'?: Array; + /** + * + * @type {string} + * @memberof DownloadInfoDto + */ + 'userId'?: string; +} /** * * @export @@ -4880,7 +4911,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration downloadArchive: async (assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'assetIdsDto' is not null or undefined assertParamExists('downloadArchive', 'assetIdsDto', assetIdsDto) - const localVarPath = `/asset/download`; + const localVarPath = `/asset/download/archive`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5379,16 +5410,15 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * - * @param {Array} [assetIds] - * @param {string} [albumId] - * @param {string} [userId] - * @param {number} [archiveSize] + * @param {DownloadInfoDto} downloadInfoDto * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getDownloadInfo: async (assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options: AxiosRequestConfig = {}): Promise => { - const localVarPath = `/asset/download`; + getDownloadInfo: async (downloadInfoDto: DownloadInfoDto, key?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'downloadInfoDto' is not null or undefined + assertParamExists('getDownloadInfo', 'downloadInfoDto', downloadInfoDto) + const localVarPath = `/asset/download/info`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -5396,7 +5426,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -5409,31 +5439,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (assetIds) { - localVarQueryParameter['assetIds'] = assetIds; - } - - if (albumId !== undefined) { - localVarQueryParameter['albumId'] = albumId; - } - - if (userId !== undefined) { - localVarQueryParameter['userId'] = userId; - } - - if (archiveSize !== undefined) { - localVarQueryParameter['archiveSize'] = archiveSize; - } - if (key !== undefined) { localVarQueryParameter['key'] = key; } + localVarHeaderParameter['Content-Type'] = 'application/json'; + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(downloadInfoDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -6141,16 +6158,13 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * - * @param {Array} [assetIds] - * @param {string} [albumId] - * @param {string} [userId] - * @param {number} [archiveSize] + * @param {DownloadInfoDto} downloadInfoDto * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getDownloadInfo(assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(assetIds, albumId, userId, archiveSize, key, options); + async getDownloadInfo(downloadInfoDto: DownloadInfoDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(downloadInfoDto, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6406,8 +6420,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(axios, basePath)); + getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -6788,31 +6802,10 @@ export interface AssetApiGetByTimeBucketRequest { export interface AssetApiGetDownloadInfoRequest { /** * - * @type {Array} + * @type {DownloadInfoDto} * @memberof AssetApiGetDownloadInfo */ - readonly assetIds?: Array - - /** - * - * @type {string} - * @memberof AssetApiGetDownloadInfo - */ - readonly albumId?: string - - /** - * - * @type {string} - * @memberof AssetApiGetDownloadInfo - */ - readonly userId?: string - - /** - * - * @type {number} - * @memberof AssetApiGetDownloadInfo - */ - readonly archiveSize?: number + readonly downloadInfoDto: DownloadInfoDto /** * @@ -7281,8 +7274,8 @@ export class AssetApi extends BaseAPI { * @throws {RequiredError} * @memberof AssetApi */ - public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 0a8ff758d2..cb29b503a0 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -1,6 +1,6 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import { downloadManager } from '$lib/stores/download'; -import { api, AssetApiGetDownloadInfoRequest, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto } from '@api'; +import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto } from '@api'; import { handleError } from './handle-error'; export const addAssetsToAlbum = async ( @@ -32,15 +32,11 @@ const downloadBlob = (data: Blob, filename: string) => { URL.revokeObjectURL(url); }; -export const downloadArchive = async ( - fileName: string, - options: Omit, - key?: string, -) => { +export const downloadArchive = async (fileName: string, options: DownloadInfoDto, key?: string) => { let downloadInfo: DownloadResponseDto | null = null; try { - const { data } = await api.assetApi.getDownloadInfo({ ...options, key }); + const { data } = await api.assetApi.getDownloadInfo({ downloadInfoDto: options, key }); downloadInfo = data; } catch (error) { handleError(error, 'Unable to download files'); From 74da15e20d353e9c0cc0221a791962454f6abf90 Mon Sep 17 00:00:00 2001 From: Sergey Kondrikov Date: Tue, 15 Aug 2023 19:02:38 +0300 Subject: [PATCH 07/14] fix(web,server): disable partner's archive access (#3695) --- server/src/domain/access/access.core.ts | 5 +++++ server/src/domain/asset/asset.service.ts | 3 +++ web/src/routes/(user)/partners/[userId]/+page.svelte | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index d815612365..3e3d4a469d 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -19,6 +19,8 @@ export enum Permission { ALBUM_SHARE = 'album.share', ALBUM_DOWNLOAD = 'album.download', + ARCHIVE_READ = 'archive.read', + LIBRARY_READ = 'library.read', LIBRARY_DOWNLOAD = 'library.download', } @@ -156,6 +158,9 @@ export class AccessCore { case Permission.ALBUM_REMOVE_ASSET: return this.repository.album.hasOwnerAccess(authUser.id, id); + case Permission.ARCHIVE_READ: + return authUser.id === id; + case Permission.LIBRARY_READ: return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id)); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index ac655b8077..2d4051b0fa 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -148,6 +148,9 @@ export class AssetService { if (dto.albumId) { await this.access.requirePermission(authUser, Permission.ALBUM_READ, [dto.albumId]); } else if (dto.userId) { + if (dto.isArchived !== false) { + await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]); + } await this.access.requirePermission(authUser, Permission.LIBRARY_READ, [dto.userId]); } else { dto.userId = authUser.id; diff --git a/web/src/routes/(user)/partners/[userId]/+page.svelte b/web/src/routes/(user)/partners/[userId]/+page.svelte index c7fd51f2a8..adc718e918 100644 --- a/web/src/routes/(user)/partners/[userId]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/+page.svelte @@ -18,7 +18,7 @@ export let data: PageData; - const assetStore = new AssetStore({ size: TimeBucketSize.Month, userId: data.partner.id }); + const assetStore = new AssetStore({ size: TimeBucketSize.Month, userId: data.partner.id, isArchived: false }); const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; From 35b4c9d3754b37d95685e1ac58dbe69974ec17f8 Mon Sep 17 00:00:00 2001 From: Vantao Date: Wed, 16 Aug 2023 00:05:00 +0800 Subject: [PATCH 08/14] doc: update README_zh_CN.md (#3701) --- README_zh_CN.md | 83 ++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/README_zh_CN.md b/README_zh_CN.md index 10a946eee3..d018a4a285 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -13,7 +13,7 @@

Immich - 高性能的自托管照片和视频备份方案

-请注意: 此README不是由Immich团队维护, 这意味着它在某一时间点不会被更新,因为我们是依靠贡献者来更新的。感谢理解。 +请注意: 此 README 不是由 Immich 团队维护, 而是依靠贡献者来更新的,这意味着它可能并不会被及时更新。感谢理解。


@@ -31,29 +31,31 @@ ## 免责声明 -- ⚠️ 本项目正在 **非常活跃** 的开发中。 -- ⚠️ 可能存在bug或者重大变更。 -- ⚠️ **不要把本软件作为你存储照片或视频的唯一方式!** +- ⚠️ 本项目正在 **非常活跃** 地开发中。 +- ⚠️ 可能存在 bug 或者随时有重大变更。 +- ⚠️ **不要把本软件作为您存储照片或视频的唯一方式。** +- ⚠️ 为了您宝贵的照片与视频,始终遵守 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 备份方案! ## 目录 -- [官方文档](https://immich.app/docs/overview/introduction) +- [官方文档](https://immich.app/docs) +- [路线图](https://github.com/orgs/immich-app/projects/1) - [示例](#示例) - [功能特性](#功能特性) - [介绍](https://immich.app/docs/overview/introduction) - [安装](https://immich.app/docs/install/requirements) - [贡献指南](https://immich.app/docs/overview/support-the-project) -- [支持本项目](#support-the-project) -- [已知问题](#known-issues) +- [支持本项目](#支持本项目) ## 官方文档 -你可以在 https://immich.app/ 找到包含安装手册的官方文档. +您可以在 https://immich.app/ 找到官方文档(包含安装手册)。 + ## 示例 -你可以在 https://demo.immich.app 访问示例. +您可以在 https://demo.immich.app 访问示例。 -在移动端, 你可以使用 `https://demo.immich.app/api`获取`服务终端链接` +在移动端, 您可以使用 `https://demo.immich.app/api` 获取 `服务终端链接` ```bash title="示例认证信息" 认证信息 @@ -62,57 +64,52 @@ ``` ``` -规格: 甲骨文免费虚拟机套餐-阿姆斯特丹 4核 2.4Ghz ARM64 CPU, 24GB RAM。 +规格: 甲骨文免费虚拟机套餐——阿姆斯特丹 4核 2.4Ghz ARM64 CPU, 24GB RAM。 ``` # 功能特性 | 功能特性 | 移动端 | 网页端 | | ------------------------------------------- | ------- | --- | -| 上传并查看照片和视频 | 是 | 是 | -| 软件运行时自动备份 | 是 | N/A | +| 上传并查看照片和视频 | 是 | 是 | +| 软件运行时自动备份 | 是 | N/A | | 选择需要备份的相册 | 是 | N/A | -| 下载照片和视频到本地 | 是 | 是 | +| 下载照片和视频到本地 | 是 | 是 | | 多用户支持 | 是 | 是 | | 相册 | 是 | 是 | | 共享相册 | 是 | 是 | | 可拖动的快速导航栏 | 是 | 是 | | 支持RAW格式 (HEIC, HEIF, DNG, Apple ProRaw) | 是 | 是 | -| 元数据视图 (EXIF, 地图) | 是 | 是 | -| 通过元数据、对象和标签进行搜索 | 是 | No | -| 管理功能 (用户管理) | N/A | 是 | -| 后台备份 | Android | N/A | +| 元数据视图(EXIF, 地图) | 是 | 是 | +| 通过元数据、对象和标签进行搜索 | 是 | 是 | +| 管理功能(用户管理) | 否 | 是 | +| 后台备份 | 是 | N/A | | 虚拟滚动 | 是 | 是 | -| OAuth支持 | 是 | 是 | -| 实时照片备份和查看 (仅iOS) | 是 | 是 | +| OAuth 支持 | 是 | 是 | +| API Keys|N/A|是| +| 实况照片备份和查看 | 仅 iOS | 是 | +|用户自定义存储结构|是|是| +|公共分享|否|是| +|归档与收藏功能|是|是| +|全局地图|否|是| +|好友分享|是|是| +|人像识别与分组|是|是| +|回忆(那年今日)|是|是| +|离线支持|是|否| +|只读相册|是|是| # 支持本项目 -我已经致力于本项目并且将我会持续更新文档、新增功能和修复问题。但是我不能一个人走下去,所以我需要你给予我走下去的动力。 +我已经致力于本项目并且将我会持续更新文档、新增功能和修复问题。但是独木不成林,我需要您给予我坚持下去的动力。 -就像我主页里面 [selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 说的一样,这是我和团队的一项艰巨的任务。我希望某一天我能够全职开发本项目,在此我希望你们能够助我梦想成真。 +就像我在 [selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 节目里说的一样,这是我和团队的一项艰巨任务。并且我希望某一天我能够全职开发本项目,在此我请求您能够助我梦想成真。 -如果你使用了本项目一段时间,并且觉得上面的话有道理,那么请你按照如下方式帮助我吧。 +如果您使用了本项目一段时间,并且觉得上面的话有道理,那么请您考虑通过下列任一方式支持我吧。 ## 捐赠 -- [按月捐赠](https://github.com/sponsors/alextran1502) via GitHub Sponsors -- [一次捐赠](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via Github Sponsors - -# 已知问题 - -## TensorFlow 构建问题 - -_这是一个针对于Proxmox的已知问题_ - -TensorFlow 不能运行在很旧的CPU架构上, 需要运行在AVX和AVX2指令集的CPU上。如果你在docker-compose的命令行中遇到了 `illegal instruction core dump`的错误, 通过如下命令检查你的CPU flag寄存器然后确保你能够看到`AVX`和`AVX2`的字样: - -```bash -more /proc/cpuinfo | grep flags -``` - -如果你在Proxmox中运行虚拟机, 虚拟机中没有启用flag寄存器。 - -你需要在虚拟机的硬件面板中把CPU类型从`kvm64`改为`host`。 - -`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host` +- 通过 GitHub Sponsors [按月捐赠](https://github.com/sponsors/alextran1502) +- 通过 Github Sponsors [单次捐赠](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) +- [Librepay](https://liberapay.com/alex.tran1502/) +- [buymeacoffee](https://www.buymeacoffee.com/altran1502) +- 比特币: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX From af1f00dff94a7f09ac76f28fec2f5c250aa6d558 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 15 Aug 2023 12:05:32 -0400 Subject: [PATCH 09/14] chore(server): cleanup (#3699) --- server/src/domain/server-info/server-info.service.ts | 6 +++--- server/src/domain/user/user.core.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index ceeb85e295..4a7e7f22bb 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -69,9 +69,9 @@ export class ServerInfoService { getSupportedMediaTypes(): ServerMediaTypesResponseDto { return { - video: [...Object.keys(mimeTypes.video)], - image: [...Object.keys(mimeTypes.image)], - sidecar: [...Object.keys(mimeTypes.sidecar)], + video: Object.keys(mimeTypes.video), + image: Object.keys(mimeTypes.image), + sidecar: Object.keys(mimeTypes.sidecar), }; } } diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index 6c59d9ca71..e7c0f7f2b1 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -60,7 +60,6 @@ export class UserCore { dto.externalPath = null; } - console.log(dto.memoriesEnabled); return this.userRepository.update(id, dto); } catch (e) { Logger.error(e, 'Failed to update user info'); From 0abbd85134035b04ec3b980661e9b497d63394e0 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 15 Aug 2023 14:34:02 -0400 Subject: [PATCH 10/14] fix(web,server): album share performance (#3698) --- server/src/domain/album/album.repository.ts | 6 ++- server/src/domain/album/album.service.spec.ts | 7 +-- server/src/domain/album/album.service.ts | 24 +++++----- .../infra/repositories/album.repository.ts | 44 ++++++++++--------- .../(user)/albums/[albumId]/+page.svelte | 6 +-- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/server/src/domain/album/album.repository.ts b/server/src/domain/album/album.repository.ts index 811b85ec9a..bc6fa37524 100644 --- a/server/src/domain/album/album.repository.ts +++ b/server/src/domain/album/album.repository.ts @@ -7,8 +7,12 @@ export interface AlbumAssetCount { assetCount: number; } +export interface AlbumInfoOptions { + withAssets: boolean; +} + export interface IAlbumRepository { - getById(id: string): Promise; + getById(id: string, options: AlbumInfoOptions): Promise; getByIds(ids: string[]): Promise; getByAssetId(ownerId: string, assetId: string): Promise; hasAsset(id: string, assetId: string): Promise; diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index 463d94d7f4..f9ab153744 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -364,6 +364,7 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), sharedUsers: [], }); + expect(albumMock.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false }); }); it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => { @@ -432,7 +433,7 @@ describe(AlbumService.name, () => { await sut.get(authStub.admin, albumStub.oneAsset.id, {}); - expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id); + expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true }); expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id); }); @@ -442,7 +443,7 @@ describe(AlbumService.name, () => { await sut.get(authStub.adminSharedLink, 'album-123', {}); - expect(albumMock.getById).toHaveBeenCalledWith('album-123'); + expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLinkId, 'album-123', @@ -455,7 +456,7 @@ describe(AlbumService.name, () => { await sut.get(authStub.user1, 'album-123', {}); - expect(albumMock.getById).toHaveBeenCalledWith('album-123'); + expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, 'album-123'); }); diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 925b87ac9a..71c11d9357 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -12,7 +12,7 @@ import { mapAlbumWithAssets, mapAlbumWithoutAssets, } from './album-response.dto'; -import { IAlbumRepository } from './album.repository'; +import { AlbumInfoOptions, IAlbumRepository } from './album.repository'; import { AddUsersDto, AlbumInfoDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto'; @Injectable() @@ -84,7 +84,7 @@ export class AlbumService { async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto) { await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); await this.albumRepository.updateThumbnails(); - return mapAlbum(await this.findOrFail(id), !dto.withoutAssets); + return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets); } async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise { @@ -111,7 +111,7 @@ export class AlbumService { async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise { await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id); - const album = await this.findOrFail(id); + const album = await this.findOrFail(id, { withAssets: true }); if (dto.albumThumbnailAssetId) { const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId); @@ -129,13 +129,13 @@ export class AlbumService { await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } }); - return mapAlbumWithAssets(updatedAlbum); + return mapAlbumWithoutAssets(updatedAlbum); } async delete(authUser: AuthUserDto, id: string): Promise { await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id); - const album = await this.albumRepository.getById(id); + const album = await this.findOrFail(id, { withAssets: false }); if (!album) { throw new BadRequestException('Album not found'); } @@ -145,7 +145,7 @@ export class AlbumService { } async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise { - const album = await this.findOrFail(id); + const album = await this.findOrFail(id, { withAssets: true }); await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); @@ -181,7 +181,7 @@ export class AlbumService { } async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise { - const album = await this.findOrFail(id); + const album = await this.findOrFail(id, { withAssets: true }); await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); @@ -225,7 +225,7 @@ export class AlbumService { async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto): Promise { await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id); - const album = await this.findOrFail(id); + const album = await this.findOrFail(id, { withAssets: false }); for (const userId of dto.sharedUserIds) { const exists = album.sharedUsers.find((user) => user.id === userId); @@ -247,7 +247,7 @@ export class AlbumService { updatedAt: new Date(), sharedUsers: album.sharedUsers, }) - .then(mapAlbumWithAssets); + .then(mapAlbumWithoutAssets); } async removeUser(authUser: AuthUserDto, id: string, userId: string | 'me'): Promise { @@ -255,7 +255,7 @@ export class AlbumService { userId = authUser.id; } - const album = await this.findOrFail(id); + const album = await this.findOrFail(id, { withAssets: false }); if (album.ownerId === userId) { throw new BadRequestException('Cannot remove album owner'); @@ -278,8 +278,8 @@ export class AlbumService { }); } - private async findOrFail(id: string) { - const album = await this.albumRepository.getById(id); + private async findOrFail(id: string, options: AlbumInfoOptions) { + const album = await this.albumRepository.getById(id, options); if (!album) { throw new BadRequestException('Album not found'); } diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts index fcf8c54c1f..5e4ac06ca0 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/infra/repositories/album.repository.ts @@ -1,7 +1,7 @@ -import { AlbumAssetCount, IAlbumRepository } from '@app/domain'; +import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, IsNull, Not, Repository } from 'typeorm'; +import { FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; import { dataSource } from '../database.config'; import { AlbumEntity, AssetEntity } from '../entities'; @@ -12,25 +12,27 @@ export class AlbumRepository implements IAlbumRepository { @InjectRepository(AlbumEntity) private repository: Repository, ) {} - getById(id: string): Promise { - return this.repository.findOne({ - where: { - id, - }, - relations: { - owner: true, - sharedUsers: true, - assets: { - exifInfo: true, - }, - sharedLinks: true, - }, - order: { - assets: { - fileCreatedAt: 'DESC', - }, - }, - }); + getById(id: string, options: AlbumInfoOptions): Promise { + const relations: FindOptionsRelations = { + owner: true, + sharedUsers: true, + assets: false, + sharedLinks: true, + }; + + const order: FindOptionsOrder = {}; + + if (options.withAssets) { + relations.assets = { + exifInfo: true, + }; + + order.assets = { + fileCreatedAt: 'DESC', + }; + } + + return this.repository.findOne({ where: { id }, relations, order }); } getByIds(ids: string[]): Promise { diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index fb169c3998..8922ab073b 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -100,7 +100,7 @@ }); const refreshAlbum = async () => { - const { data } = await api.albumApi.getAlbumInfo({ id: album.id, withoutAssets: false }); + const { data } = await api.albumApi.getAlbumInfo({ id: album.id, withoutAssets: true }); album = data; }; @@ -261,9 +261,9 @@ } }; - const handleUpdateDescription = (description: string) => { + const handleUpdateDescription = async (description: string) => { try { - api.albumApi.updateAlbumInfo({ + await api.albumApi.updateAlbumInfo({ id: album.id, updateAlbumDto: { description, From c27c12d975e94cf21cf5fcc40e0eac63bda6a36b Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Wed, 16 Aug 2023 02:06:49 +0200 Subject: [PATCH 11/14] fix(server): people sorting (#3713) --- server/src/domain/person/person.repository.ts | 1 + server/src/domain/person/person.service.spec.ts | 6 +++--- server/src/domain/person/person.service.ts | 6 ++++-- server/src/infra/repositories/person.repository.ts | 14 ++++++++++---- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/server/src/domain/person/person.repository.ts b/server/src/domain/person/person.repository.ts index 3c8432be1e..973d940cd6 100644 --- a/server/src/domain/person/person.repository.ts +++ b/server/src/domain/person/person.repository.ts @@ -4,6 +4,7 @@ export const IPersonRepository = 'IPersonRepository'; export interface PersonSearchOptions { minimumFaceCount: number; + withHidden: boolean; } export interface UpdateFacesData { diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 7aaa875f27..e5bca7c830 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -47,7 +47,7 @@ describe(PersonService.name, () => { visible: 1, people: [responseDto], }); - expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 }); + expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: false }); }); it('should get all visible people with thumbnails', async () => { personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]); @@ -56,7 +56,7 @@ describe(PersonService.name, () => { visible: 1, people: [responseDto], }); - expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 }); + expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: false }); }); it('should get all hidden and visible people with thumbnails', async () => { personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]); @@ -73,7 +73,7 @@ describe(PersonService.name, () => { }, ], }); - expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 }); + expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: true }); }); }); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 0f66447f13..187ef3358d 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -26,8 +26,10 @@ export class PersonService { ) {} async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise { - const people = await this.repository.getAll(authUser.id, { minimumFaceCount: 1 }); - + const people = await this.repository.getAll(authUser.id, { + minimumFaceCount: 1, + withHidden: dto.withHidden || false, + }); const persons: PersonResponseDto[] = people // with thumbnails .filter((person) => !!person.thumbnailPath) diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index 8595484365..1203bba381 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -51,16 +51,22 @@ export class PersonRepository implements IPersonRepository { } getAll(userId: string, options?: PersonSearchOptions): Promise { - return this.personRepository + const queryBuilder = this.personRepository .createQueryBuilder('person') .leftJoin('person.faces', 'face') .where('person.ownerId = :userId', { userId }) - .orderBy('COUNT(face.assetId)', 'DESC') + .orderBy('person.isHidden', 'ASC') + .addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC') + .addOrderBy('COUNT(face.assetId)', 'DESC') .addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST') .having('COUNT(face.assetId) >= :faces', { faces: options?.minimumFaceCount || 1 }) .groupBy('person.id') - .limit(500) - .getMany(); + .limit(500); + if (!options?.withHidden) { + queryBuilder.andWhere('person.isHidden = false'); + } + + return queryBuilder.getMany(); } getAllWithoutFaces(): Promise { From 4762fd83d414cc239124a9d2902c18be99a1d2f5 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 15 Aug 2023 21:34:57 -0400 Subject: [PATCH 12/14] fix(server): link live photos after metadata extraction finishes (#3702) * fix(server): link live photos after metadata extraction finishes * chore: fix test --------- Co-authored-by: Alex --- server/src/domain/album/album.repository.ts | 2 + server/src/domain/job/job.constants.ts | 2 + server/src/domain/job/job.repository.ts | 1 + server/src/domain/job/job.service.spec.ts | 4 ++ server/src/domain/job/job.service.ts | 6 +- .../infra/repositories/album.repository.ts | 17 +++++- server/src/microservices/app.service.ts | 1 + .../metadata-extraction.processor.ts | 60 +++++++++++-------- .../repositories/album.repository.mock.ts | 1 + 9 files changed, 64 insertions(+), 30 deletions(-) diff --git a/server/src/domain/album/album.repository.ts b/server/src/domain/album/album.repository.ts index bc6fa37524..d501964ef5 100644 --- a/server/src/domain/album/album.repository.ts +++ b/server/src/domain/album/album.repository.ts @@ -16,6 +16,8 @@ export interface IAlbumRepository { getByIds(ids: string[]): Promise; getByAssetId(ownerId: string, assetId: string): Promise; hasAsset(id: string, assetId: string): Promise; + /** Remove an asset from _all_ albums */ + removeAsset(id: string): Promise; getAssetCountForIds(ids: string[]): Promise; getInvalidThumbnail(): Promise; getOwned(ownerId: string): Promise; diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index a02248b306..02fa588c9b 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -32,6 +32,7 @@ export enum JobName { // metadata QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', METADATA_EXTRACTION = 'metadata-extraction', + LINK_LIVE_PHOTOS = 'link-live-photos', // user deletion USER_DELETION = 'user-deletion', @@ -98,6 +99,7 @@ export const JOBS_TO_QUEUE: Record = { // metadata [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, + [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION, // storage template [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION, diff --git a/server/src/domain/job/job.repository.ts b/server/src/domain/job/job.repository.ts index c088a2eedf..f605bef4b4 100644 --- a/server/src/domain/job/job.repository.ts +++ b/server/src/domain/job/job.repository.ts @@ -45,6 +45,7 @@ export type JobItem = // Metadata Extraction | { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob } | { name: JobName.METADATA_EXTRACTION; data: IEntityJob } + | { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob } // Sidecar Scanning | { name: JobName.QUEUE_SIDECAR; data: IBaseJob } diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts index a8c6ec9dc6..503440a5cf 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/domain/job/job.service.spec.ts @@ -252,6 +252,10 @@ describe(JobService.name, () => { }, { item: { name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }, + jobs: [JobName.LINK_LIVE_PHOTOS], + }, + { + item: { name: JobName.LINK_LIVE_PHOTOS, data: { id: 'asset-1' } }, jobs: [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, JobName.SEARCH_INDEX_ASSET], }, { diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index df79491367..1c82908911 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -149,6 +149,10 @@ export class JobService { break; case JobName.METADATA_EXTRACTION: + await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data }); + break; + + case JobName.LINK_LIVE_PHOTOS: await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: item.data }); break; @@ -186,7 +190,7 @@ export class JobService { case JobName.CLASSIFY_IMAGE: case JobName.ENCODE_CLIP: case JobName.RECOGNIZE_FACES: - case JobName.METADATA_EXTRACTION: + case JobName.LINK_LIVE_PHOTOS: await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [item.data.id] } }); break; } diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts index 5e4ac06ca0..c59589710b 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/infra/repositories/album.repository.ts @@ -1,7 +1,7 @@ import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; import { dataSource } from '../database.config'; import { AlbumEntity, AssetEntity } from '../entities'; @@ -10,6 +10,7 @@ export class AlbumRepository implements IAlbumRepository { constructor( @InjectRepository(AssetEntity) private assetRepository: Repository, @InjectRepository(AlbumEntity) private repository: Repository, + @InjectDataSource() private dataSource: DataSource, ) {} getById(id: string, options: AlbumInfoOptions): Promise { @@ -84,7 +85,7 @@ export class AlbumRepository implements IAlbumRepository { */ async getInvalidThumbnail(): Promise { // Using dataSource, because there is no direct access to albums_assets_assets. - const albumHasAssets = dataSource + const albumHasAssets = this.dataSource .createQueryBuilder() .select('1') .from('albums_assets_assets', 'albums_assets') @@ -150,6 +151,16 @@ export class AlbumRepository implements IAlbumRepository { }); } + async removeAsset(assetId: string): Promise { + // Using dataSource, because there is no direct access to albums_assets_assets. + await this.dataSource + .createQueryBuilder() + .delete() + .from('albums_assets_assets') + .where('"albums_assets_assets"."assetsId" = :assetId', { assetId }) + .execute(); + } + hasAsset(id: string, assetId: string): Promise { return this.repository.exist({ where: { diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index a8f30e1888..1204a6ebdd 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -66,6 +66,7 @@ export class AppService { [JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data), [JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataProcessor.handleQueueMetadataExtraction(data), [JobName.METADATA_EXTRACTION]: (data) => this.metadataProcessor.handleMetadataExtraction(data), + [JobName.LINK_LIVE_PHOTOS]: (data) => this.metadataProcessor.handleLivePhotoLinking(data), [JobName.QUEUE_RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleQueueRecognizeFaces(data), [JobName.RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleRecognizeFaces(data), [JobName.GENERATE_FACE_THUMBNAIL]: (data) => this.facialRecognitionService.handleGenerateFaceThumbnail(data), diff --git a/server/src/microservices/processors/metadata-extraction.processor.ts b/server/src/microservices/processors/metadata-extraction.processor.ts index a5f3ed4dd3..7c58f7102e 100644 --- a/server/src/microservices/processors/metadata-extraction.processor.ts +++ b/server/src/microservices/processors/metadata-extraction.processor.ts @@ -1,4 +1,5 @@ import { + IAlbumRepository, IAssetRepository, IBaseJob, ICryptoRepository, @@ -59,6 +60,7 @@ export class MetadataExtractionProcessor { constructor( @Inject(IAssetRepository) private assetRepository: IAssetRepository, + @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @@ -92,6 +94,38 @@ export class MetadataExtractionProcessor { } } + async handleLivePhotoLinking(job: IEntityJob) { + const { id } = job; + const [asset] = await this.assetRepository.getByIds([id]); + if (!asset?.exifInfo) { + return false; + } + + if (!asset.exifInfo.livePhotoCID) { + return true; + } + + const otherType = asset.type === AssetType.VIDEO ? AssetType.IMAGE : AssetType.VIDEO; + const match = await this.assetRepository.findLivePhotoMatch({ + livePhotoCID: asset.exifInfo.livePhotoCID, + ownerId: asset.ownerId, + otherAssetId: asset.id, + type: otherType, + }); + + if (!match) { + return true; + } + + const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset]; + + await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }); + await this.assetRepository.save({ id: motionAsset.id, isVisible: false }); + await this.albumRepository.removeAsset(motionAsset.id); + + return true; + } + async handleQueueMetadataExtraction(job: IBaseJob) { const { force } = job; const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { @@ -351,19 +385,6 @@ export class MetadataExtractionProcessor { } newExif.livePhotoCID = getExifProperty('MediaGroupUUID'); - if (newExif.livePhotoCID && !asset.livePhotoVideoId) { - const motionAsset = await this.assetRepository.findLivePhotoMatch({ - livePhotoCID: newExif.livePhotoCID, - otherAssetId: asset.id, - ownerId: asset.ownerId, - type: AssetType.VIDEO, - }); - if (motionAsset) { - await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id }); - await this.assetRepository.save({ id: motionAsset.id, isVisible: false }); - } - } - await this.applyReverseGeocoding(asset, newExif); /** @@ -428,19 +449,6 @@ export class MetadataExtractionProcessor { newExif.fps = null; newExif.livePhotoCID = exifData?.ContentIdentifier || null; - if (newExif.livePhotoCID) { - const photoAsset = await this.assetRepository.findLivePhotoMatch({ - livePhotoCID: newExif.livePhotoCID, - ownerId: asset.ownerId, - otherAssetId: asset.id, - type: AssetType.IMAGE, - }); - if (photoAsset) { - await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: asset.id }); - await this.assetRepository.save({ id: asset.id, isVisible: false }); - } - } - if (videoTags && videoTags['location']) { const location = videoTags['location'] as string; const locationRegex = /([+-][0-9]+\.[0-9]+)([+-][0-9]+\.[0-9]+)\/$/; diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts index 8656fa64a1..3d42630dfb 100644 --- a/server/test/repositories/album.repository.mock.ts +++ b/server/test/repositories/album.repository.mock.ts @@ -12,6 +12,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked => { getNotShared: jest.fn(), deleteAll: jest.fn(), getAll: jest.fn(), + removeAsset: jest.fn(), hasAsset: jest.fn(), create: jest.fn(), update: jest.fn(), From bc66b1a556bf51a496397a4a618fb415e2183985 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Wed, 16 Aug 2023 03:46:23 +0200 Subject: [PATCH 13/14] fix(web): user-management layout (#3704) * fix: user-management layout * better user form scrollbar --------- Co-authored-by: Alex Tran --- .../components/forms/edit-user-form.svelte | 2 +- web/src/routes/admin/+layout.svelte | 4 +-- .../routes/admin/user-management/+page.svelte | 36 +++++++++---------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index 760c5f48ef..5881488508 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -67,7 +67,7 @@
-
-
+
+
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 5dbe71aca3..586d5a3042 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -168,11 +168,11 @@ class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary" > - Email - First name - Last name - Can import - Action + Email + First name + Last name + Can import + Action @@ -187,10 +187,10 @@ : 'bg-immich-bg dark:bg-immich-dark-gray/50' }`} > - {user.email} - {user.firstName} - {user.lastName} - + {user.email} + {user.firstName} + {user.lastName} +
{#if user.externalPath} @@ -199,7 +199,7 @@ {/if}
- + {#if !isDeleted(user)}