0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-02-11 01:18:24 -05:00

fix(server, web): harden auto pick album thumbnails (#918)

This commit is contained in:
Jason Rasmussen 2022-11-04 09:41:04 -04:00 committed by GitHub
parent 2782dae518
commit d696ce4e41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 48 additions and 45 deletions

View file

@ -1,9 +1,9 @@
import { AlbumEntity } from '@app/database/entities/album.entity'; import { AlbumEntity } from '@app/database/entities/album.entity';
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity'; import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
import { UserAlbumEntity } from '@app/database/entities/user-album.entity'; import { UserAlbumEntity } from '@app/database/entities/user-album.entity';
import { BadRequestException, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository, SelectQueryBuilder, DataSource } from 'typeorm'; import { In, Repository, SelectQueryBuilder, DataSource } from 'typeorm';
import { AddAssetsDto } from './dto/add-assets.dto'; import { AddAssetsDto } from './dto/add-assets.dto';
import { AddUsersDto } from './dto/add-users.dto'; import { AddUsersDto } from './dto/add-users.dto';
import { CreateAlbumDto } from './dto/create-album.dto'; import { CreateAlbumDto } from './dto/create-album.dto';
@ -11,7 +11,7 @@ import { GetAlbumsDto } from './dto/get-albums.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto'; import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import {AddAssetsResponseDto} from "./response-dto/add-assets-response.dto"; import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
export interface IAlbumRepository { export interface IAlbumRepository {
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>; create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
@ -20,7 +20,7 @@ export interface IAlbumRepository {
delete(album: AlbumEntity): Promise<void>; delete(album: AlbumEntity): Promise<void>;
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>; addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
removeUser(album: AlbumEntity, userId: string): Promise<void>; removeUser(album: AlbumEntity, userId: string): Promise<void>;
removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<AlbumEntity>; removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>;
addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>; addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>;
updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>; updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>; getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>;
@ -237,28 +237,13 @@ export class AlbumRepository implements IAlbumRepository {
await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId }); await this.userAlbumRepository.delete({ albumId: album.id, sharedUserId: userId });
} }
async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<AlbumEntity> { async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> {
let deleteAssetCount = 0; const res = await this.assetAlbumRepository.delete({
// TODO: should probably do a single delete query? albumId: album.id,
for (const assetId of removeAssetsDto.assetIds) { assetId: In(removeAssetsDto.assetIds),
const res = await this.assetAlbumRepository.delete({ albumId: album.id, assetId: assetId }); });
if (res.affected == 1) deleteAssetCount++;
}
// TODO: No need to return boolean if using a singe delete query return res.affected || 0;
if (deleteAssetCount == removeAssetsDto.assetIds.length) {
const retAlbum = (await this.get(album.id)) as AlbumEntity;
if (retAlbum?.assets?.length === 0) {
// is empty album
await this.albumRepository.update(album.id, { albumThumbnailAssetId: null });
retAlbum.albumThumbnailAssetId = null;
}
return retAlbum;
} else {
throw new BadRequestException('Some assets were not found in the album');
}
} }
async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto> { async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto> {
@ -267,7 +252,7 @@ export class AlbumRepository implements IAlbumRepository {
for (const assetId of addAssetsDto.assetIds) { for (const assetId of addAssetsDto.assetIds) {
// Album already contains that asset // Album already contains that asset
if (album.assets?.some(a => a.assetId === assetId)) { if (album.assets?.some((a) => a.assetId === assetId)) {
alreadyExisting.push(assetId); alreadyExisting.push(assetId);
continue; continue;
} }
@ -288,7 +273,7 @@ export class AlbumRepository implements IAlbumRepository {
return { return {
successfullyAdded: newRecords.length, successfullyAdded: newRecords.length,
alreadyInAlbum: alreadyExisting alreadyInAlbum: alreadyExisting,
}; };
} }

View file

@ -65,11 +65,13 @@ export class AlbumService {
* @returns All Shared Album And Its Members * @returns All Shared Album And Its Members
*/ */
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> { async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
let albums: AlbumEntity[];
if (typeof getAlbumsDto.assetId === 'string') { if (typeof getAlbumsDto.assetId === 'string') {
const albums = await this._albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId); albums = await this._albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
return albums.map(mapAlbumExcludeAssetInfo); } else {
albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
} }
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
for (const album of albums) { for (const album of albums) {
await this._checkValidThumbnail(album); await this._checkValidThumbnail(album);
@ -112,8 +114,18 @@ export class AlbumService {
albumId: string, albumId: string,
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId }); const album = await this._getAlbum({ authUser, albumId });
const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto); const deletedCount = await this._albumRepository.removeAssets(album, removeAssetsDto);
return mapAlbum(updateAlbum); const newAlbum = await this._getAlbum({ authUser, albumId });
if (newAlbum) {
await this._checkValidThumbnail(newAlbum);
}
if (deletedCount !== removeAssetsDto.assetIds.length) {
throw new BadRequestException('Some assets were not found in the album');
}
return mapAlbum(newAlbum);
} }
async addAssetsToAlbum( async addAssetsToAlbum(
@ -178,17 +190,17 @@ export class AlbumService {
} }
} }
async _checkValidThumbnail(album: AlbumEntity): Promise<AlbumEntity> { async _checkValidThumbnail(album: AlbumEntity) {
const assetId = album.albumThumbnailAssetId; const assets = album.assets || [];
if (assetId) { const valid = assets.some((asset) => asset.assetId === album.albumThumbnailAssetId);
try { if (!valid) {
await this._assetRepository.getById(assetId); let dto: UpdateAlbumDto = {};
} catch (e) { if (assets.length > 0) {
album.albumThumbnailAssetId = null; const albumThumbnailAssetId = assets[0].assetId;
return await this._albumRepository.updateAlbum(album, {}); dto = { albumThumbnailAssetId };
} }
await this._albumRepository.updateAlbum(album, dto);
album.albumThumbnailAssetId = dto.albumThumbnailAssetId || null;
} }
return album;
} }
} }

View file

@ -43,8 +43,8 @@ describe('AlbumCard component', () => {
const albumNameElement = sut.getByTestId('album-name'); const albumNameElement = sut.getByTestId('album-name');
const albumDetailsElement = sut.getByTestId('album-details'); const albumDetailsElement = sut.getByTestId('album-details');
const detailsText = `${count} items` + (shared ? ' . Shared' : ''); const detailsText = `${count} items` + (shared ? ' . Shared' : '');
// TODO: is this a bug?
expect(albumImgElement).toHaveAttribute('src', '/api/asset/thumbnail/null?format=WEBP'); expect(albumImgElement).toHaveAttribute('src', 'no-thumbnail.png');
expect(albumImgElement).toHaveAttribute('alt', album.id); expect(albumImgElement).toHaveAttribute('alt', album.id);
await waitFor(() => expect(albumImgElement).toHaveAttribute('src', 'no-thumbnail.png')); await waitFor(() => expect(albumImgElement).toHaveAttribute('src', 'no-thumbnail.png'));

View file

@ -19,7 +19,13 @@
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
const NO_THUMBNAIL = 'no-thumbnail.png';
let imageData = `/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`; let imageData = `/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`;
if (!album.albumThumbnailAssetId) {
imageData = NO_THUMBNAIL;
}
const dispatchClick = createEventDispatcher<OnClick>(); const dispatchClick = createEventDispatcher<OnClick>();
const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>(); const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>();
@ -45,7 +51,7 @@
}; };
onMount(async () => { onMount(async () => {
imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || 'no-thumbnail.png'; imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || NO_THUMBNAIL;
}); });
</script> </script>