diff --git a/src/backend/model/database/sql/GalleryManager.ts b/src/backend/model/database/sql/GalleryManager.ts index 8dae59bc..1ab132cd 100644 --- a/src/backend/model/database/sql/GalleryManager.ts +++ b/src/backend/model/database/sql/GalleryManager.ts @@ -18,6 +18,7 @@ import {FaceRegionEntry} from './enitites/FaceRegionEntry'; import {ObjectManagers} from '../../ObjectManagers'; import {DuplicatesDTO} from '../../../../common/entities/DuplicatesDTO'; import {ServerConfig} from '../../../../common/config/private/PrivateConfig'; +import DatabaseType = ServerConfig.DatabaseType; const LOG_TAG = '[GalleryManager]'; @@ -202,7 +203,8 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { path: directoryParent }) .leftJoinAndSelect('directory.directories', 'directories') - .leftJoinAndSelect('directory.media', 'media'); + .leftJoinAndSelect('directory.media', 'media') + .orderBy('media.metadata.creationDate', 'DESC'); if (Config.Client.MetaFile.enabled === true) { query.leftJoinAndSelect('directory.metaFile', 'metaFile'); @@ -211,6 +213,33 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { return await query.getOne(); } + protected async fillPreviewFromSubDir(connection: Connection, dir: DirectoryEntity): Promise { + dir.media = []; + const query = connection + .getRepository(MediaEntity) + .createQueryBuilder('media') + .innerJoinAndSelect('media.directory', 'directory'); + + if (Config.Server.Database.type === DatabaseType.mysql) { + query.where('directory.path like :path || \'%\'', { + path: (DiskMangerWorker.pathFromParent(dir)) + }); + } else { + query.where('directory.path GLOB :path', { + path: DiskMangerWorker.pathFromParent(dir) + '*' + }); + } + dir.preview = await query.orderBy('media.metadata.creationDate', 'DESC') + .limit(1) + .getOne(); + + if (dir.preview) { + dir.preview.directory = dir; + dir.preview.readyThumbnails = []; + dir.preview.readyIcon = false; + } + } + protected async fillParentDir(connection: Connection, dir: DirectoryEntity): Promise { if (dir.media) { const indexedFaces = await connection.getRepository(FaceRegionEntry) @@ -232,14 +261,15 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { .filter(fe => fe.media.id === dir.media[i].id) .map(f => ({box: f.box, name: f.person.name})); } - + if (dir.media.length > 0) { + dir.preview = dir.media[0]; + } else { + await this.fillPreviewFromSubDir(connection, dir); + } } if (dir.directories) { for (let i = 0; i < dir.directories.length; i++) { - const _path = dir.path; - const currentRoot = (_path === './' ? '' : _path); - const dirName = currentRoot + dir.name + path.sep; dir.directories[i].media = []; dir.directories[i].preview = await connection .getRepository(MediaEntity) @@ -248,9 +278,6 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { .where('media.directory = :dir', { dir: dir.directories[i].id }) - .orWhere('directory.path like :parentPath||\'%\'', { - parentPath: dirName - }) .orderBy('media.metadata.creationDate', 'DESC') .limit(1) .getOne(); @@ -260,6 +287,8 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { dir.directories[i].preview.directory = dir.directories[i]; dir.directories[i].preview.readyThumbnails = []; dir.directories[i].preview.readyIcon = false; + } else { + await this.fillPreviewFromSubDir(connection, dir.directories[i]); } } } diff --git a/src/backend/model/database/sql/SQLConnection.ts b/src/backend/model/database/sql/SQLConnection.ts index e2d1eaa7..a633462e 100644 --- a/src/backend/model/database/sql/SQLConnection.ts +++ b/src/backend/model/database/sql/SQLConnection.ts @@ -19,6 +19,7 @@ import {PersonEntry} from './enitites/PersonEntry'; import {Utils} from '../../../../common/Utils'; import * as path from 'path'; import {ServerConfig} from '../../../../common/config/private/PrivateConfig'; +import DatabaseType = ServerConfig.DatabaseType; export class SQLConnection { diff --git a/src/backend/model/threading/DiskMangerWorker.ts b/src/backend/model/threading/DiskMangerWorker.ts index 56c3f5d2..3b2d2b53 100644 --- a/src/backend/model/threading/DiskMangerWorker.ts +++ b/src/backend/model/threading/DiskMangerWorker.ts @@ -148,10 +148,14 @@ export class DiskMangerWorker { if (!directory.preview) { directory.preview = photo; } + // add the preview photo to the list of media, so it will be saved to the DB + // and can be queried to populate previews, + // otherwise we do not return media list that is only partial + directory.media.push(photo); + if (settings.previewOnly === true) { break; } - directory.media.push(photo); } else if (VideoProcessing.isVideo(fullFilePath)) { if (Config.Client.Media.Video.enabled === false || settings.noVideo === true || settings.previewOnly === true) { diff --git a/src/common/Utils.ts b/src/common/Utils.ts index c63ec835..bb62caec 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -24,7 +24,7 @@ export class Utils { } - static removeNullOrEmptyObj(obj: any) { + static removeNullOrEmptyObj(obj: T): T { if (typeof obj !== 'object' || obj == null) { return obj; } diff --git a/src/common/entities/DirectoryDTO.ts b/src/common/entities/DirectoryDTO.ts index 58a89e3e..f7cbc477 100644 --- a/src/common/entities/DirectoryDTO.ts +++ b/src/common/entities/DirectoryDTO.ts @@ -40,7 +40,7 @@ export module DirectoryDTO { } }; - export const removeReferences = (dir: DirectoryDTO): void => { + export const removeReferences = (dir: DirectoryDTO): DirectoryDTO => { if (dir.media) { dir.media.forEach((media: MediaDTO) => { media.directory = null; @@ -61,6 +61,8 @@ export module DirectoryDTO { }); } + return dir; + }; export const filterPhotos = (dir: DirectoryDTO): PhotoDTO[] => { return dir.media.filter(m => MediaDTO.isPhoto(m)); diff --git a/test/backend/unit/model/sql/IndexingManager.ts b/test/backend/unit/model/sql/IndexingManager.ts index bd73543a..56684eb8 100644 --- a/test/backend/unit/model/sql/IndexingManager.ts +++ b/test/backend/unit/model/sql/IndexingManager.ts @@ -69,7 +69,9 @@ describe('IndexingManager', (sqlHelper: SQLTestHelper) => { }); const setPartial = (dir: DirectoryDTO) => { - dir.preview = dir.media[0]; + if (!dir.preview && dir.media && dir.media.length > 0) { + dir.preview = dir.media[0]; + } dir.isPartial = true; delete dir.directories; delete dir.metaFile; @@ -191,6 +193,62 @@ describe('IndexingManager', (sqlHelper: SQLTestHelper) => { }); + it('should select preview', async () => { + const selectDirectory = async (_gm: GalleryManagerTest, dir: DirectoryDTO) => { + const conn = await SQLConnection.getConnection(); + const selected = await _gm.selectParentDir(conn, dir.name, dir.path); + await _gm.fillParentDir(conn, selected); + + DirectoryDTO.removeReferences(selected); + removeIds(selected); + return selected; + }; + + const gm = new GalleryManagerTest(); + const im = new IndexingManagerTest(); + + + const parent = TestHelper.getRandomizedDirectoryEntry(null, 'parent'); + + + const checkParent = async () => { + const selected = await selectDirectory(gm, parent); + const cloned = Utils.removeNullOrEmptyObj(Utils.clone(parent)); + if (cloned.directories) { + cloned.directories.forEach(d => setPartial(d)); + } + expect(Utils.clone(Utils.removeNullOrEmptyObj(selected))) + .to.deep.equalInAnyOrder(cloned); + }; + + const saveToDBAndCheck = async (dir: DirectoryDTO) => { + DirectoryDTO.removeReferences(parent); + await im.saveToDB(Utils.clone(dir)); + await checkParent(); + DirectoryDTO.addReferences(parent); + }; + + await saveToDBAndCheck(parent); + + const subDir1 = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir'); + await saveToDBAndCheck(parent); + + const p1 = TestHelper.getRandomizedPhotoEntry(subDir1, 'subPhoto1', 0); + await saveToDBAndCheck(subDir1); + + const subDir2 = TestHelper.getRandomizedDirectoryEntry(parent, 'subDir2'); + await saveToDBAndCheck(parent); + + const p2 = TestHelper.getRandomizedPhotoEntry(subDir2, 'subPhoto2', 0); + await saveToDBAndCheck(subDir2); + + const p = TestHelper.getRandomizedPhotoEntry(parent, 'photo', 0); + await saveToDBAndCheck(parent); + + + }); + + it('should save parent after child', async () => { const gm = new GalleryManagerTest(); const im = new IndexingManagerTest(); diff --git a/test/backend/unit/model/sql/PersonManager.ts b/test/backend/unit/model/sql/PersonManager.ts index 1cc88db5..4cae6025 100644 --- a/test/backend/unit/model/sql/PersonManager.ts +++ b/test/backend/unit/model/sql/PersonManager.ts @@ -2,14 +2,13 @@ import {expect} from 'chai'; import {PersonManager} from '../../../../../src/backend/model/database/sql/PersonManager'; import {SQLTestHelper} from '../../../SQLTestHelper'; import {TestHelper} from './TestHelper'; -import {PhotoDTO, PhotoMetadata} from '../../../../../src/common/entities/PhotoDTO'; +import {PhotoDTO} from '../../../../../src/common/entities/PhotoDTO'; import {Utils} from '../../../../../src/common/Utils'; import {PersonWithSampleRegion} from '../../../../../src/common/entities/PersonDTO'; import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {VideoDTO} from '../../../../../src/common/entities/VideoDTO'; import {SQLConnection} from '../../../../../src/backend/model/database/sql/SQLConnection'; import {PersonEntry} from '../../../../../src/backend/model/database/sql/enitites/PersonEntry'; -import {MediaDTO} from '../../../../../src/common/entities/MediaDTO'; // to help WebStorm to handle the test cases @@ -36,17 +35,17 @@ describe('PersonManager', (sqlHelper: SQLTestHelper) => { const setUpSqlDB = async () => { await sqlHelper.initDB(); const directory: DirectoryDTO = TestHelper.getDirectoryEntry(); - TestHelper.getPhotoEntry1(directory); - TestHelper.getPhotoEntry2(directory); + p = TestHelper.getPhotoEntry1(directory); + p2 = TestHelper.getPhotoEntry2(directory); const pFaceLess = TestHelper.getPhotoEntry3(directory); delete pFaceLess.metadata.faces; - TestHelper.getVideoEntry1(directory); + v = TestHelper.getVideoEntry1(directory); dir = await SQLTestHelper.persistTestDir(directory); - p = dir.media[0]; - p2 = dir.media[1]; + p = dir.media.filter(m => m.name === p.name)[0]; + p2 = dir.media.filter(m => m.name === p2.name)[0]; p_faceLess = dir.media[2]; - v = dir.media[3]; + v = dir.media.filter(m => m.name === v.name)[0]; savedPerson = await (await SQLConnection.getConnection()).getRepository(PersonEntry).find({ relations: ['sampleRegion', 'sampleRegion.media', @@ -64,7 +63,6 @@ describe('PersonManager', (sqlHelper: SQLTestHelper) => { }); - it('should get person', async () => { const pm = new PersonManager(); const person = Utils.clone(savedPerson[0]); diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index 8c18a759..30a26565 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -90,17 +90,17 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { const directory: DirectoryDTO = TestHelper.getDirectoryEntry(); const subDir = TestHelper.getDirectoryEntry(directory, 'The Phantom Menace'); const subDir2 = TestHelper.getDirectoryEntry(directory, 'Return of the Jedi'); - TestHelper.getPhotoEntry1(directory); - TestHelper.getPhotoEntry2(directory); - TestHelper.getPhotoEntry4(subDir2); + p = TestHelper.getPhotoEntry1(directory); + p2 = TestHelper.getPhotoEntry2(directory); + p4 = TestHelper.getPhotoEntry4(subDir2); const pFaceLess = TestHelper.getPhotoEntry3(subDir); delete pFaceLess.metadata.faces; - TestHelper.getVideoEntry1(directory); + v = TestHelper.getVideoEntry1(directory); dir = await SQLTestHelper.persistTestDir(directory); - p = dir.media[0]; - p2 = dir.media[1]; - v = dir.media[2]; + p = dir.media.filter(m => m.name === p.name)[0]; + p2 = dir.media.filter(m => m.name === p2.name)[0]; + v = dir.media.filter(m => m.name === v.name)[0]; p4 = dir.directories[1].media[0]; p_faceLess = dir.directories[0].media[0]; }; @@ -176,9 +176,11 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { const searchifyMedia = (m: MediaDTO): MediaDTO => { const tmpM = m.directory.media; const tmpD = m.directory.directories; + const tmpP = m.directory.preview; const tmpMT = m.directory.metaFile; delete m.directory.directories; delete m.directory.media; + delete m.directory.preview; delete m.directory.metaFile; const ret = Utils.clone(m); if ((ret.metadata as PhotoMetadata).faces && !(ret.metadata as PhotoMetadata).faces.length) { @@ -186,6 +188,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { } m.directory.directories = tmpD; m.directory.media = tmpM; + m.directory.preview = tmpP; m.directory.metaFile = tmpMT; return ret; }; @@ -946,7 +949,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], - media: [p, p2, p_faceLess, v], + media: [p, p2, p_faceLess, v], metaFile: [], resultOverflow: false })); diff --git a/test/backend/unit/model/sql/TestHelper.ts b/test/backend/unit/model/sql/TestHelper.ts index 47129ead..0a0088b2 100644 --- a/test/backend/unit/model/sql/TestHelper.ts +++ b/test/backend/unit/model/sql/TestHelper.ts @@ -24,6 +24,8 @@ import {DiskMangerWorker} from '../../../../../src/backend/model/threading/DiskM export class TestHelper { + static creationCounter = 0; + public static getDirectoryEntry(parent: DirectoryDTO = null, name = 'wars dir') { const dir = new DirectoryEntity(); @@ -156,7 +158,6 @@ export class TestHelper { return p; } - public static getPhotoEntry2(dir: DirectoryDTO) { const p = TestHelper.getPhotoEntry(dir); @@ -249,7 +250,6 @@ export class TestHelper { return p; } - public static getRandomizedDirectoryEntry(parent: DirectoryDTO = null, forceStr: string = null) { const dir: DirectoryDTO = { @@ -263,7 +263,7 @@ export class TestHelper { media: [], lastModified: Date.now(), lastScanned: null, - parent: null + parent: parent }; if (parent !== null) { dir.path = DiskMangerWorker.pathFromParent(parent); @@ -272,7 +272,6 @@ export class TestHelper { return dir; } - public static getRandomizedGPXEntry(dir: DirectoryDTO, forceStr: string = null): FileDTO { const d: FileDTO = { id: null, @@ -283,7 +282,6 @@ export class TestHelper { return d; } - public static getRandomizedFace(media: PhotoDTO, forceStr: string = null) { const rndStr = () => { return forceStr + '_' + Math.random().toString(36).substring(7); @@ -348,7 +346,7 @@ export class TestHelper { cameraData: cd, positionData: pd, size: sd, - creationDate: Date.now(), + creationDate: Date.now() + ++TestHelper.creationCounter, fileSize: rndInt(10000), orientation: OrientationTypes.TOP_LEFT, caption: rndStr(), @@ -356,7 +354,7 @@ export class TestHelper { }; - const d: PhotoDTO = { + const p: PhotoDTO = { id: null, name: rndStr() + '.jpg', directory: dir, @@ -366,11 +364,27 @@ export class TestHelper { }; for (let i = 0; i < faces; i++) { - this.getRandomizedFace(d, 'Person ' + i); + this.getRandomizedFace(p, 'Person ' + i); + } + + dir.media.push(p); + TestHelper.updatePreview(dir); + return p; + } + + static updatePreview(dir: DirectoryDTO) { + if (dir.media.length > 0) { + dir.preview = dir.media.sort((a, b) => b.metadata.creationDate - a.metadata.creationDate)[0]; + } else { + const filtered = dir.directories.filter(d => d.preview).map(d => d.preview); + if (filtered.length > 0) { + dir.preview = filtered.sort((a, b) => b.metadata.creationDate - a.metadata.creationDate)[0]; + } + } + if (dir.parent) { + TestHelper.updatePreview(dir.parent); } - dir.media.push(d); - return d; }