diff --git a/backend/model/ObjectManagerRepository.ts b/backend/model/ObjectManagerRepository.ts index 01a8cfd..c66b12d 100644 --- a/backend/model/ObjectManagerRepository.ts +++ b/backend/model/ObjectManagerRepository.ts @@ -113,7 +113,7 @@ export class ObjectManagerRepository { const UserManager = require('./sql/UserManager').UserManager; const SearchManager = require('./sql/SearchManager').SearchManager; const SharingManager = require('./sql/SharingManager').SharingManager; - const IndexingTaskManager = require('./sql/IndexingManager').IndexingTaskManager; + const IndexingTaskManager = require('./sql/IndexingTaskManager').IndexingTaskManager; const IndexingManager = require('./sql/IndexingManager').IndexingManager; const PersonManager = require('./sql/PersonManager').PersonManager; ObjectManagerRepository.getInstance().GalleryManager = new GalleryManager(); diff --git a/backend/model/sql/SearchManager.ts b/backend/model/sql/SearchManager.ts index b2c44de..982c072 100644 --- a/backend/model/sql/SearchManager.ts +++ b/backend/model/sql/SearchManager.ts @@ -6,6 +6,8 @@ import {PhotoEntity} from './enitites/PhotoEntity'; import {DirectoryEntity} from './enitites/DirectoryEntity'; import {MediaEntity} from './enitites/MediaEntity'; import {VideoEntity} from './enitites/VideoEntity'; +import {PersonEntry} from './enitites/PersonEntry'; +import {FaceRegionEntry} from './enitites/FaceRegionEntry'; export class SearchManager implements ISearchManager { @@ -29,7 +31,7 @@ export class SearchManager implements ISearchManager { let result: AutoCompleteItem[] = []; const photoRepository = connection.getRepository(PhotoEntity); const videoRepository = connection.getRepository(VideoEntity); - const mediaRepository = connection.getRepository(MediaEntity); + const personRepository = connection.getRepository(PersonEntry); const directoryRepository = connection.getRepository(DirectoryEntity); @@ -45,6 +47,14 @@ export class SearchManager implements ISearchManager { .filter(k => k.toLowerCase().indexOf(text.toLowerCase()) !== -1), SearchTypes.keyword)); }); + result = result.concat(this.encapsulateAutoComplete((await personRepository + .createQueryBuilder('person') + .select('DISTINCT(person.name)') + .where('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .limit(5) + .getRawMany()) + .map(r => r.name), SearchTypes.person)); + (await photoRepository .createQueryBuilder('photo') .select('photo.metadata.positionData.country as country, ' + @@ -112,16 +122,19 @@ export class SearchManager implements ISearchManager { resultOverflow: false }; - let repostiroy = connection.getRepository(MediaEntity); + let repository = connection.getRepository(MediaEntity); + const faceRepository = connection.getRepository(FaceRegionEntry); if (searchType === SearchTypes.photo) { - repostiroy = connection.getRepository(PhotoEntity); + repository = connection.getRepository(PhotoEntity); } else if (searchType === SearchTypes.video) { - repostiroy = connection.getRepository(VideoEntity); + repository = connection.getRepository(VideoEntity); } - const query = repostiroy.createQueryBuilder('media') - .innerJoinAndSelect('media.directory', 'directory') + const query = repository.createQueryBuilder('media') + .leftJoinAndSelect('media.directory', 'directory') + .leftJoin('media.metadata.faces', 'faces') + .leftJoin('faces.person', 'person') .orderBy('media.metadata.creationDate', 'ASC'); @@ -136,6 +149,9 @@ export class SearchManager implements ISearchManager { if (!searchType || searchType === SearchTypes.photo) { query.orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); } + if (!searchType || searchType === SearchTypes.person) { + query.orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + } if (!searchType || searchType === SearchTypes.position) { query.orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) @@ -147,9 +163,20 @@ export class SearchManager implements ISearchManager { query.orWhere('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); } - result.media = await query - .limit(2001) - .getMany(); + + result.media = (await query + .limit(5000).getMany()).slice(0, 2001); + + for (let i = 0; i < result.media.length; i++) { + const faces = (await faceRepository + .createQueryBuilder('faces') + .leftJoinAndSelect('faces.person', 'person') + .where('faces.media = :media', {media: result.media[i].id}) + .getMany()).map(fE => ({name: fE.person.name, box: fE.box})); + if (faces.length > 0) { + result.media[i].metadata.faces = faces; + } + } if (result.media.length > 2000) { result.resultOverflow = true; @@ -181,6 +208,8 @@ export class SearchManager implements ISearchManager { resultOverflow: false }; + const faceRepository = connection.getRepository(FaceRegionEntry); + result.media = await connection .getRepository(MediaEntity) .createQueryBuilder('media') @@ -191,10 +220,23 @@ export class SearchManager implements ISearchManager { .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .innerJoinAndSelect('media.directory', 'directory') + .leftJoin('media.metadata.faces', 'faces') + .leftJoin('faces.person', 'person') .limit(10) .getMany(); + for (let i = 0; i < result.media.length; i++) { + const faces = (await faceRepository + .createQueryBuilder('faces') + .leftJoinAndSelect('faces.person', 'person') + .where('faces.media = :media', {media: result.media[i].id}) + .getMany()).map(fE => ({name: fE.person.name, box: fE.box})); + if (faces.length > 0) { + result.media[i].metadata.faces = faces; + } + } result.directories = await connection .getRepository(DirectoryEntity) diff --git a/backend/model/threading/MetadataLoader.ts b/backend/model/threading/MetadataLoader.ts index 27123fc..34af889 100644 --- a/backend/model/threading/MetadataLoader.ts +++ b/backend/model/threading/MetadataLoader.ts @@ -4,7 +4,7 @@ import {Config} from '../../../common/config/private/Config'; import {Logger} from '../../Logger'; import * as fs from 'fs'; import * as sizeOf from 'image-size'; -import {OrientationTypes, ExifParserFactory} from 'ts-exif-parser'; +import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser'; import {IptcParser} from 'ts-node-iptc'; import {FFmpegFactory} from '../FFmpegFactory'; import {FfprobeData} from 'fluent-ffmpeg'; @@ -147,10 +147,16 @@ export class MetadataLoader { try { const iptcData = IptcParser.parse(data); - if (iptcData.country_or_primary_location_name || iptcData.province_or_state || iptcData.city) { + if (iptcData.country_or_primary_location_name) { metadata.positionData = metadata.positionData || {}; metadata.positionData.country = iptcData.country_or_primary_location_name.replace(/\0/g, '').trim(); + } + if (iptcData.province_or_state) { + metadata.positionData = metadata.positionData || {}; metadata.positionData.state = iptcData.province_or_state.replace(/\0/g, '').trim(); + } + if (iptcData.city) { + metadata.positionData = metadata.positionData || {}; metadata.positionData.city = iptcData.city.replace(/\0/g, '').trim(); } if (iptcData.caption) { @@ -160,7 +166,7 @@ export class MetadataLoader { metadata.creationDate = (iptcData.date_time ? iptcData.date_time.getTime() : metadata.creationDate); } catch (err) { - // Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err); + Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err); } metadata.creationDate = metadata.creationDate || 0; diff --git a/common/entities/AutoCompleteItem.ts b/common/entities/AutoCompleteItem.ts index 9fe6e5a..f61323d 100644 --- a/common/entities/AutoCompleteItem.ts +++ b/common/entities/AutoCompleteItem.ts @@ -1,9 +1,10 @@ export enum SearchTypes { directory = 1, - keyword = 2, - position = 3, - photo = 4, - video = 5 + person = 2, + keyword = 3, + position = 5, + photo = 6, + video = 7 } export class AutoCompleteItem { diff --git a/demo/images/IMG_5910.jpg b/demo/images/IMG_5910.jpg index 852d0a6..2ca467a 100644 Binary files a/demo/images/IMG_5910.jpg and b/demo/images/IMG_5910.jpg differ diff --git a/demo/images/IMG_6253.jpg b/demo/images/IMG_6253.jpg index 507ac5a..9cc007c 100644 Binary files a/demo/images/IMG_6253.jpg and b/demo/images/IMG_6253.jpg differ diff --git a/demo/images/IMG_6297.jpg b/demo/images/IMG_6297.jpg index 605fbe8..72d6d28 100644 Binary files a/demo/images/IMG_6297.jpg and b/demo/images/IMG_6297.jpg differ diff --git a/demo/images/IMG_9398-2.jpg b/demo/images/IMG_9398-2.jpg index 8865b28..44cbc0d 100644 Binary files a/demo/images/IMG_9398-2.jpg and b/demo/images/IMG_9398-2.jpg differ diff --git a/demo/images/IMG_9516.jpg b/demo/images/IMG_9516.jpg index 1fd0193..3c3255c 100644 Binary files a/demo/images/IMG_9516.jpg and b/demo/images/IMG_9516.jpg differ diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.css b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.css index 62ed865..ea791be 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.css +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.css @@ -92,7 +92,7 @@ a { width: 100%; } -.video-indicator{ +.video-indicator { font-size: large; padding: 5px; position: absolute; @@ -102,12 +102,16 @@ a { background-color: rgba(0, 0, 0, 0.5); color: white; transition: background-color .3s ease-out; - -moz-transition: background-color .3s ease-out; + -moz-transition: background-color .3s ease-out; -webkit-transition: background-color .3s ease-out; - -o-transition: background-color .3s ease-out; - -ms-transition: background-color .3s ease-out; + -o-transition: background-color .3s ease-out; + -ms-transition: background-color .3s ease-out; } -.photo-container:hover .video-indicator{ +.photo-container:hover .video-indicator { background-color: rgba(0, 0, 0, 0.8); } + +.photo-keywords .oi-person{ + margin-right: 2px; +} diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html index f786253..d842117 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html @@ -36,12 +36,18 @@ -
- +
+ #{{keyword}} - #{{keyword}} - , + [routerLink]="['/search', keyword.value, {type: SearchTypes[keyword.type]}]" [ngSwitch]="keyword.type"> + #{{keyword.value}} + + #{{keyword.value}} + ,
diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts index 8af6b46..43e56ed 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts @@ -5,9 +5,8 @@ import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {RouterLink} from '@angular/router'; import {Thumbnail, ThumbnailManagerService} from '../../thumbnailManager.service'; import {Config} from '../../../../../common/config/public/Config'; -import {AnimationBuilder} from '@angular/animations'; import {PageHelper} from '../../../model/page.helper'; -import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; +import {PhotoDTO, PhotoMetadata} from '../../../../../common/entities/PhotoDTO'; @Component({ selector: 'app-gallery-grid-photo', @@ -22,6 +21,7 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { @ViewChild('photoContainer') container: ElementRef; thumbnail: Thumbnail; + keywords: { value: string, type: SearchTypes }[] = null; infoBar = { marginTop: 0, visible: false, @@ -34,17 +34,40 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { wasInView: boolean = null; - constructor(private thumbnailService: ThumbnailManagerService, - private _animationBuilder: AnimationBuilder) { + constructor(private thumbnailService: ThumbnailManagerService) { this.SearchTypes = SearchTypes; this.searchEnabled = Config.Client.Search.enabled; } - ngOnInit() { - this.thumbnail = this.thumbnailService.getThumbnail(this.gridPhoto); + get ScrollListener(): boolean { + return !this.thumbnail.Available && !this.thumbnail.Error; } + get Title(): string { + if (Config.Client.Other.captionFirstNaming === false) { + return this.gridPhoto.media.name; + } + if ((this.gridPhoto.media).metadata.caption) { + if ((this.gridPhoto.media).metadata.caption.length > 20) { + return (this.gridPhoto.media).metadata.caption.substring(0, 17) + '...'; + } + return (this.gridPhoto.media).metadata.caption; + } + return this.gridPhoto.media.name; + } + + ngOnInit() { + this.thumbnail = this.thumbnailService.getThumbnail(this.gridPhoto); + const metadata = this.gridPhoto.media.metadata as PhotoMetadata; + if ((metadata.keywords && metadata.keywords.length > 0) || + (metadata.faces && metadata.faces.length > 0)) { + this.keywords = (metadata.faces || []).map(f => ({value: f.name, type: SearchTypes.person})) + .concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword}))); + } + + } + ngOnDestroy() { this.thumbnail.destroy(); @@ -53,16 +76,11 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { } } - isInView(): boolean { return PageHelper.ScrollY < this.container.nativeElement.offsetTop + this.container.nativeElement.clientHeight && PageHelper.ScrollY + window.innerHeight > this.container.nativeElement.offsetTop; } - get ScrollListener(): boolean { - return !this.thumbnail.Available && !this.thumbnail.Error; - } - onScroll() { if (this.thumbnail.Available === true || this.thumbnail.Error === true) { return; @@ -74,7 +92,6 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { } } - getPositionText(): string { if (!this.gridPhoto || !this.gridPhoto.isPhoto()) { return ''; @@ -84,7 +101,6 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { (this.gridPhoto.media).metadata.positionData.country; } - mouseOver() { this.infoBar.visible = true; if (this.animationTimer != null) { @@ -124,19 +140,6 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { } - get Title(): string { - if (Config.Client.Other.captionFirstNaming === false) { - return this.gridPhoto.media.name; - } - if ((this.gridPhoto.media).metadata.caption) { - if ((this.gridPhoto.media).metadata.caption.length > 20) { - return (this.gridPhoto.media).metadata.caption.substring(0, 17) + '...'; - } - return (this.gridPhoto.media).metadata.caption; - } - return this.gridPhoto.media.name; - } - /* onImageLoad() { this.loading.show = false; diff --git a/frontend/app/gallery/navigator/navigator.gallery.component.html b/frontend/app/gallery/navigator/navigator.gallery.component.html index 9a45dee..c2056bc 100644 --- a/frontend/app/gallery/navigator/navigator.gallery.component.html +++ b/frontend/app/gallery/navigator/navigator.gallery.component.html @@ -15,6 +15,7 @@ + {{searchResult.searchText}} diff --git a/frontend/app/gallery/search/search.gallery.component.html b/frontend/app/gallery/search/search.gallery.component.html index 40de478..2c85a97 100644 --- a/frontend/app/gallery/search/search.gallery.component.html +++ b/frontend/app/gallery/search/search.gallery.component.html @@ -25,6 +25,7 @@ + {{item.preText}}{{item.highLightText}}{{item.postText}}