Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31987bc043 | ||
|
|
23f0eb6fe8 | ||
|
|
0994575bf3 | ||
|
|
f4a12acd29 | ||
|
|
335216f6dd | ||
|
|
5a9acbc05b |
2
cli/src/api/open-api/api.ts
generated
2
cli/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -11,6 +11,6 @@ Running into an issue or have a question? Try the following:
|
||||
3. Search through existing [GitHub Issues][github-issues].
|
||||
4. Open a help ticket on [Discord][discord-link].
|
||||
|
||||
[github-issues]: https://github.com/immich-app/immich/releases
|
||||
[github-issues]: https://github.com/immich-app/immich/issues
|
||||
[github-releases]: https://github.com/immich-app/immich/releases
|
||||
[discord-link]: https://discord.com/invite/D8JsnBEuKb
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.82.0"
|
||||
version = "1.82.1"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -36,7 +36,7 @@ platform :android do
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 106,
|
||||
"android.injected.version.name" => "1.82.0",
|
||||
"android.injected.version.name" => "1.82.1",
|
||||
}
|
||||
)
|
||||
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')
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.82.0"
|
||||
version_number: "1.82.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.82.0
|
||||
- API version: 1.82.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.82.0+106
|
||||
version: 1.82.1+106
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
|
||||
environment:
|
||||
|
||||
@@ -5379,7 +5379,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.82.0",
|
||||
"version": "1.82.1",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.82.0",
|
||||
"version": "1.82.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.82.0",
|
||||
"version": "1.82.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.82.0",
|
||||
"version": "1.82.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import {
|
||||
albumStub,
|
||||
assetStub,
|
||||
authStub,
|
||||
IAccessRepositoryMock,
|
||||
newAccessRepositoryMock,
|
||||
@@ -225,7 +224,7 @@ describe(AlbumService.name, () => {
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(albumMock.hasAsset).toHaveBeenCalledWith(albumStub.oneAsset.id, 'not-in-album');
|
||||
expect(albumMock.hasAsset).toHaveBeenCalledWith({ albumId: 'album-4', assetId: 'not-in-album' });
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -461,6 +460,7 @@ describe(AlbumService.name, () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.hasAsset.mockResolvedValue(false);
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
@@ -473,9 +473,12 @@ describe(AlbumService.name, () => {
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
||||
albumId: 'album-123',
|
||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set the thumbnail if the album has one already', async () => {
|
||||
@@ -490,9 +493,9 @@ describe(AlbumService.name, () => {
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
assets: [{ id: 'asset-1' }],
|
||||
albumThumbnailAssetId: 'asset-id',
|
||||
});
|
||||
expect(albumMock.addAssets).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow a shared user to add assets', async () => {
|
||||
@@ -512,9 +515,12 @@ describe(AlbumService.name, () => {
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
assets: [{ id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
||||
albumId: 'album-123',
|
||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow a shared link user to add assets', async () => {
|
||||
@@ -523,6 +529,7 @@ describe(AlbumService.name, () => {
|
||||
accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.hasAsset.mockResolvedValue(false);
|
||||
|
||||
await expect(
|
||||
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
|
||||
@@ -535,9 +542,12 @@ describe(AlbumService.name, () => {
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
||||
albumId: 'album-123',
|
||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
||||
});
|
||||
|
||||
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.sharedLinkId,
|
||||
@@ -550,6 +560,7 @@ describe(AlbumService.name, () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.hasAsset.mockResolvedValue(false);
|
||||
|
||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-1' },
|
||||
@@ -558,10 +569,8 @@ describe(AlbumService.name, () => {
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
assets: [assetStub.image, { id: 'asset-1' }],
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
|
||||
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
|
||||
});
|
||||
|
||||
@@ -569,6 +578,7 @@ describe(AlbumService.name, () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.hasAsset.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE },
|
||||
@@ -620,17 +630,14 @@ describe(AlbumService.name, () => {
|
||||
it('should allow the owner to remove assets', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.hasAsset.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
assets: [],
|
||||
albumThumbnailAssetId: null,
|
||||
});
|
||||
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
||||
expect(albumMock.removeAssets).toHaveBeenCalledWith({ assetIds: ['asset-id'], albumId: 'album-123' });
|
||||
});
|
||||
|
||||
it('should skip assets not in the album', async () => {
|
||||
@@ -647,9 +654,14 @@ describe(AlbumService.name, () => {
|
||||
it('should skip assets without user permission to remove', async () => {
|
||||
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||
albumMock.hasAsset.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NO_PERMISSION },
|
||||
{
|
||||
success: false,
|
||||
id: 'asset-id',
|
||||
error: BulkIdErrorReason.NO_PERMISSION,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
@@ -658,6 +670,7 @@ describe(AlbumService.name, () => {
|
||||
it('should reset the thumbnail if it is removed', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
||||
albumMock.hasAsset.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-id' },
|
||||
@@ -666,9 +679,8 @@ describe(AlbumService.name, () => {
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
assets: [assetStub.withLocation],
|
||||
albumThumbnailAssetId: assetStub.withLocation.id,
|
||||
});
|
||||
expect(albumMock.updateThumbnails).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ export class AlbumService {
|
||||
const album = await this.findOrFail(id, { withAssets: true });
|
||||
|
||||
if (dto.albumThumbnailAssetId) {
|
||||
const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId);
|
||||
const valid = await this.albumRepository.hasAsset({ albumId: id, assetId: dto.albumThumbnailAssetId });
|
||||
if (!valid) {
|
||||
throw new BadRequestException('Invalid album thumbnail');
|
||||
}
|
||||
@@ -148,35 +148,34 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
const album = await this.findOrFail(id, { withAssets: true });
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
|
||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
for (const id of dto.ids) {
|
||||
const hasAsset = album.assets.find((asset) => asset.id === id);
|
||||
for (const assetId of dto.ids) {
|
||||
const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId });
|
||||
if (hasAsset) {
|
||||
results.push({ id, success: false, error: BulkIdErrorReason.DUPLICATE });
|
||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE });
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, id);
|
||||
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId);
|
||||
if (!hasAccess) {
|
||||
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({ id, success: true });
|
||||
album.assets.push({ id } as AssetEntity);
|
||||
results.push({ id: assetId, success: true });
|
||||
}
|
||||
|
||||
const newAsset = results.find(({ success }) => success);
|
||||
if (newAsset) {
|
||||
const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||
if (newAssetIds.length > 0) {
|
||||
await this.albumRepository.addAssets({ albumId: id, assetIds: newAssetIds });
|
||||
await this.albumRepository.update({
|
||||
id,
|
||||
assets: album.assets,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAsset.id,
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAssetIds[0],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,42 +183,37 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
const album = await this.findOrFail(id, { withAssets: true });
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
|
||||
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
|
||||
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
for (const id of dto.ids) {
|
||||
const hasAsset = album.assets.find((asset) => asset.id === id);
|
||||
for (const assetId of dto.ids) {
|
||||
const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId });
|
||||
if (!hasAsset) {
|
||||
results.push({ id, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasAccess = await this.access.hasAny(authUser, [
|
||||
{ permission: Permission.ALBUM_REMOVE_ASSET, id },
|
||||
{ permission: Permission.ASSET_SHARE, id },
|
||||
{ permission: Permission.ALBUM_REMOVE_ASSET, id: assetId },
|
||||
{ permission: Permission.ASSET_SHARE, id: assetId },
|
||||
]);
|
||||
if (!hasAccess) {
|
||||
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({ id, success: true });
|
||||
album.assets = album.assets.filter((asset) => asset.id !== id);
|
||||
if (album.albumThumbnailAssetId === id) {
|
||||
album.albumThumbnailAssetId = null;
|
||||
}
|
||||
results.push({ id: assetId, success: true });
|
||||
}
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
if (hasSuccess) {
|
||||
await this.albumRepository.update({
|
||||
id,
|
||||
assets: album.assets,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId || album.assets[0]?.id || null,
|
||||
});
|
||||
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||
if (removedIds.length > 0) {
|
||||
await this.albumRepository.removeAssets({ albumId: id, assetIds: removedIds });
|
||||
await this.albumRepository.update({ id, updatedAt: new Date() });
|
||||
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
@@ -11,13 +11,24 @@ export interface AlbumInfoOptions {
|
||||
withAssets: boolean;
|
||||
}
|
||||
|
||||
export interface AlbumAsset {
|
||||
albumId: string;
|
||||
assetId: string;
|
||||
}
|
||||
|
||||
export interface AlbumAssets {
|
||||
albumId: string;
|
||||
assetIds: string[];
|
||||
}
|
||||
|
||||
export interface IAlbumRepository {
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
hasAsset(id: string, assetId: string): Promise<boolean>;
|
||||
/** Remove an asset from _all_ albums */
|
||||
removeAsset(id: string): Promise<void>;
|
||||
addAssets(assets: AlbumAssets): Promise<void>;
|
||||
hasAsset(asset: AlbumAsset): Promise<boolean>;
|
||||
removeAsset(assetId: string): Promise<void>;
|
||||
removeAssets(assets: AlbumAssets): Promise<void>;
|
||||
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
getInvalidThumbnail(): Promise<string[]>;
|
||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||
|
||||
@@ -6,7 +6,7 @@ export class SystemConfigMachineLearningDto {
|
||||
@IsBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
@IsUrl({ require_tld: false, allow_underscores: true })
|
||||
@ValidateIf((dto) => dto.enabled)
|
||||
url!: string;
|
||||
|
||||
|
||||
@@ -189,6 +189,15 @@ describe(SystemConfigService.name, () => {
|
||||
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||
});
|
||||
|
||||
it('should allow underscores in the machine learning url', async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
|
||||
configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(partialConfig)));
|
||||
|
||||
const config = await sut.getConfig();
|
||||
expect(config.machineLearning.url).toEqual('immich_machine_learning');
|
||||
});
|
||||
|
||||
const tests = [
|
||||
{ should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } },
|
||||
{ should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
|
||||
import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
|
||||
@@ -168,16 +168,27 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('albums_assets_assets')
|
||||
.where('"albums_assets_assets"."assetsId" = :assetId', { assetId })
|
||||
.where('"albums_assets_assets"."assetsId" = :assetId', { assetId });
|
||||
}
|
||||
|
||||
async removeAssets(asset: AlbumAssets): Promise<void> {
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('albums_assets_assets')
|
||||
.where({
|
||||
albumsId: asset.albumId,
|
||||
assetsId: In(asset.assetIds),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
hasAsset(id: string, assetId: string): Promise<boolean> {
|
||||
hasAsset(asset: AlbumAsset): Promise<boolean> {
|
||||
return this.repository.exist({
|
||||
where: {
|
||||
id,
|
||||
id: asset.albumId,
|
||||
assets: {
|
||||
id: assetId,
|
||||
id: asset.assetId,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
@@ -186,6 +197,15 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async addAssets({ albumId, assetIds }: AlbumAssets): Promise<void> {
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into('albums_assets_assets', ['albumsId', 'assetsId'])
|
||||
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
|
||||
.execute();
|
||||
}
|
||||
|
||||
async create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.save(album);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
||||
softDeleteAll: jest.fn(),
|
||||
deleteAll: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
addAssets: jest.fn(),
|
||||
removeAsset: jest.fn(),
|
||||
removeAssets: jest.fn(),
|
||||
hasAsset: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
|
||||
2
web/src/api/open-api/api.ts
generated
2
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.82.0
|
||||
* The version of the OpenAPI document: 1.82.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let admin = false;
|
||||
|
||||
$: scrollbarClass = scrollbar ? 'immich-scrollbar p-4 pb-8' : 'scrollbar-hidden pl-4';
|
||||
$: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
|
||||
</script>
|
||||
|
||||
<header>
|
||||
@@ -32,20 +33,19 @@
|
||||
<SideBar />
|
||||
{/if}
|
||||
</slot>
|
||||
<slot name="content">
|
||||
{#if title}
|
||||
<section class="relative">
|
||||
<div
|
||||
class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<p class="font-medium">{title}</p>
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
|
||||
<div class="{scrollbarClass} absolute top-16 h-[calc(100%-theme(spacing.16))] w-full overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
<section class="relative">
|
||||
{#if title}
|
||||
<div
|
||||
class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<p class="font-medium">{title}</p>
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
{/if}
|
||||
</slot>
|
||||
|
||||
<div class="{scrollbarClass} absolute {hasTitleClass} w-full overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
export let assetStore: AssetStore;
|
||||
export let assetInteractionStore: AssetInteractionStore;
|
||||
export let removeAction: AssetAction | null = null;
|
||||
|
||||
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
|
||||
export let forceDelete = false;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export const openWebsocketConnection = () => {
|
||||
try {
|
||||
const websocket = io('', {
|
||||
path: '/api/socket.io',
|
||||
transports: ['polling'],
|
||||
reconnection: true,
|
||||
forceNew: true,
|
||||
autoConnect: true,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
|
||||
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
|
||||
<EmptyPlaceholder
|
||||
text="Archive photos and videos to hide them from your Photos view"
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
|
||||
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE}>
|
||||
<EmptyPlaceholder
|
||||
text="Add favorites to quickly find your best pictures and videos"
|
||||
|
||||
@@ -48,39 +48,36 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
|
||||
<svelte:fragment slot="header">
|
||||
{#if $isMultiSelectState}
|
||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
|
||||
<CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
|
||||
<SelectAllAssets {assetStore} {assetInteractionStore} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets
|
||||
on:escape={() => (handleEscapeKey = true)}
|
||||
onAssetDelete={(assetId) => assetStore.removeAsset(assetId)}
|
||||
/>
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<DownloadAction menuItem />
|
||||
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
|
||||
<AssetJobActions />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{#if $isMultiSelectState}
|
||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
|
||||
<CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
|
||||
<SelectAllAssets {assetStore} {assetInteractionStore} />
|
||||
<AssetSelectContextMenu icon={Plus} title="Add">
|
||||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</AssetSelectContextMenu>
|
||||
<DeleteAssets
|
||||
on:escape={() => (handleEscapeKey = true)}
|
||||
onAssetDelete={(assetId) => assetStore.removeAsset(assetId)}
|
||||
/>
|
||||
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<DownloadAction menuItem />
|
||||
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
|
||||
<AssetJobActions />
|
||||
</AssetSelectContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{/if}
|
||||
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}>
|
||||
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} on:escape={handleEscape}>
|
||||
{#if data.user.memoriesEnabled}
|
||||
<MemoryLane />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} on:escape={handleEscape}>
|
||||
{#if data.user.memoriesEnabled}
|
||||
<MemoryLane />
|
||||
{/if}
|
||||
<EmptyPlaceholder
|
||||
text="CLICK TO UPLOAD YOUR FIRST PHOTO"
|
||||
actionHandler={() => openFileUploadDialog()}
|
||||
slot="empty"
|
||||
/>
|
||||
</AssetGrid>
|
||||
</svelte:fragment>
|
||||
<EmptyPlaceholder
|
||||
text="CLICK TO UPLOAD YOUR FIRST PHOTO"
|
||||
actionHandler={() => openFileUploadDialog()}
|
||||
slot="empty"
|
||||
/>
|
||||
</AssetGrid>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{/if}
|
||||
|
||||
{#if $featureFlags.loaded && $featureFlags.trash}
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
|
||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<LinkButton on:click={handleRestoreTrash}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
|
||||
<AssetGrid forceDelete {assetStore} {assetInteractionStore}>
|
||||
<p class="font-medium text-gray-500/60 dark:text-gray-300/60">
|
||||
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 py-4">
|
||||
Trashed items will be permanently deleted after {$serverConfig.trashDays} days.
|
||||
</p>
|
||||
<EmptyPlaceholder
|
||||
|
||||
Reference in New Issue
Block a user