From 8295542941a7b99305f6dcf483730260663e58ed Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 20 Dec 2023 16:51:53 +0100 Subject: [PATCH] feat(cli): Add existing assets to album and allow album name (#5838) * Allow building and installing cli * feat: add format fix * docs: remove cli folder * feat: use immich scoped package * feat: rewrite cli readme * docs: add info on running without building * cleanup * chore: remove import functionality from cli * feat: add logout to cli * docs: add todo for file format from server * docs: add compilation step to cli * fix: success message spacing * feat: can create albums * fix: add check step to cli * fix: typos * feat: pull file formats from server * chore: use crawl service from server * chore: fix lint * docs: add cli documentation * chore: rename ignore pattern * chore: add version number to cli * feat: use sdk * fix: cleanup * feat: album name on windows * chore: remove skipped asset field * feat: add more info to server-info command * chore: cleanup * wip * chore: remove unneeded packages * e2e test can start * git ignore for geocode in cli * add cli e2e to github actions * can do e2e tests in the cli * simplify e2e test * cleanup * set matrix strategy in workflow * run npm ci in server * choose different working directory * check out submodules too * increase test timeout * set node version * cli docker e2e tests * fix cli docker file * run cli e2e in correct folder * set docker context * correct docker build * remove cli from dockerignore * chore: fix docs links * feat: add cli v2 milestone * fix: set correct cli date * remove submodule * chore: add npmignore * chore(cli): push to npm * fix: server e2e * run npm ci in server * remove state from e2e * run npm ci in server * reshuffle docker compose files * use new e2e composes in makefile * increase test timeout to 10 minutes * make github actions run makefile e2e tests * cleanup github test names * assert on server version * chore: split cli e2e tests into one file per command * chore: set cli release working dir * chore: add repo url to npmjs * chore: bump node setup to v4 * chore: normalize the github url * check e2e code in lint * fix lint * test key login flow * feat: allow configurable config dir * fix session service tests * create missing dir * cleanup * bump cli version to 2.0.4 * remove form-data * feat: allow single files as argument * add version option * bump dependencies * fix lint * wip use axios as upload * version bump * cApiTALiZaTiON * don't touch package lock * wip: don't use job queues * don't use make for cli e2e * fix server e2e * feat: always skip hashing when adding albums * feat: create album with specific name * check asset duplication before adding to album * update documentation * use correct check for when to create albums --------- Co-authored-by: Alex Co-authored-by: Jason Rasmussen --- cli/src/commands/upload.ts | 33 +++++++++++++++++++---- cli/src/cores/dto/upload-options-dto.ts | 1 + cli/src/index.ts | 5 ++++ cli/test/e2e/upload.e2e-spec.ts | 35 +++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/cli/src/commands/upload.ts b/cli/src/commands/upload.ts index b0f192dca2..da3146f95e 100644 --- a/cli/src/commands/upload.ts +++ b/cli/src/commands/upload.ts @@ -62,6 +62,10 @@ export default class Upload extends BaseCommand { // Compute total size first await asset.process(); totalSize += asset.fileSize; + + if (options.albumName) { + asset.albumName = options.albumName; + } } const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data; @@ -76,6 +80,10 @@ export default class Upload extends BaseCommand { }); let skipUpload = false; + + let skipAsset = false; + let existingAssetId: string | undefined = undefined; + if (!options.skipHash) { const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] }; @@ -84,14 +92,24 @@ export default class Upload extends BaseCommand { }); skipUpload = checkResponse.data.results[0].action === 'reject'; + + const isDuplicate = checkResponse.data.results[0].reason === 'duplicate'; + if (isDuplicate) { + existingAssetId = checkResponse.data.results[0].assetId; + } + + skipAsset = skipUpload && !isDuplicate; } - if (!skipUpload) { + if (!skipAsset) { if (!options.dryRun) { - const formData = asset.getUploadFormData(); - const res = await this.uploadAsset(formData); + if (!skipUpload) { + const formData = asset.getUploadFormData(); + const res = await this.uploadAsset(formData); + existingAssetId = res.data.id; + } - if (options.album && asset.albumName) { + if ((options.album || options.albumName) && asset.albumName !== undefined) { let album = existingAlbums.find((album) => album.albumName === asset.albumName); if (!album) { const res = await this.immichApi.albumApi.createAlbum({ @@ -101,7 +119,12 @@ export default class Upload extends BaseCommand { existingAlbums.push(album); } - await this.immichApi.albumApi.addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: [res.data.id] } }); + if (existingAssetId) { + await this.immichApi.albumApi.addAssetsToAlbum({ + id: album.id, + bulkIdsDto: { ids: [existingAssetId] }, + }); + } } } diff --git a/cli/src/cores/dto/upload-options-dto.ts b/cli/src/cores/dto/upload-options-dto.ts index 8b5fbdc4a7..943f321a24 100644 --- a/cli/src/cores/dto/upload-options-dto.ts +++ b/cli/src/cores/dto/upload-options-dto.ts @@ -5,5 +5,6 @@ export class UploadOptionsDto { skipHash? = false; delete? = false; album? = false; + albumName? = ''; includeHidden? = false; } diff --git a/cli/src/index.ts b/cli/src/index.ts index 3e5b74c47d..e27c379158 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -32,6 +32,11 @@ program .env('IMMICH_AUTO_CREATE_ALBUM') .default(false), ) + .addOption( + new Option('-A, --album-name ', 'Add all assets to specified album') + .env('IMMICH_ALBUM_NAME') + .conflicts('album'), + ) .addOption( new Option('-n, --dry-run', "Don't perform any actions, just show what will be done") .env('IMMICH_DRY_RUN') diff --git a/cli/test/e2e/upload.e2e-spec.ts b/cli/test/e2e/upload.e2e-spec.ts index 738d9125d1..04b005f47c 100644 --- a/cli/test/e2e/upload.e2e-spec.ts +++ b/cli/test/e2e/upload.e2e-spec.ts @@ -35,6 +35,12 @@ describe(`upload (e2e)`, () => { expect(assets.length).toBeGreaterThan(4); }); + it('should not create a new album', async () => { + await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true }); + const albums = await api.albumApi.getAllAlbums(server, admin.accessToken); + expect(albums.length).toEqual(0); + }); + it('should create album from folder name', async () => { await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { recursive: true, @@ -46,4 +52,33 @@ describe(`upload (e2e)`, () => { const natureAlbum = albums[0]; expect(natureAlbum.albumName).toEqual('nature'); }); + + it('should add existing assets to album', async () => { + await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { + recursive: true, + }); + + // Upload again, but this time add to album + await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { + recursive: true, + album: true, + }); + + const albums = await api.albumApi.getAllAlbums(server, admin.accessToken); + expect(albums.length).toEqual(1); + const natureAlbum = albums[0]; + expect(natureAlbum.albumName).toEqual('nature'); + }); + + it('should upload to the specified album name', async () => { + await new Upload(CLI_BASE_OPTIONS).run([`${IMMICH_TEST_ASSET_PATH}/albums/nature/`], { + recursive: true, + albumName: 'testAlbum', + }); + + const albums = await api.albumApi.getAllAlbums(server, admin.accessToken); + expect(albums.length).toEqual(1); + const testAlbum = albums[0]; + expect(testAlbum.albumName).toEqual('testAlbum'); + }); });