1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

adding persons support for frontend

This commit is contained in:
Patrik J. Braun 2019-01-13 20:18:18 +01:00
parent 550d8d4f5f
commit 49bea44b05
14 changed files with 117 additions and 53 deletions

View File

@ -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();

View File

@ -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)

View File

@ -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 = <number>(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;

View File

@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 883 KiB

After

Width:  |  Height:  |  Size: 884 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

After

Width:  |  Height:  |  Size: 530 KiB

View File

@ -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;
}

View File

@ -36,12 +36,18 @@
</ng-template>
</div>
<div class="photo-keywords" *ngIf="gridPhoto.media.metadata.keywords && gridPhoto.media.metadata.keywords.length">
<ng-template ngFor let-keyword [ngForOf]="gridPhoto.media.metadata.keywords" let-last="last">
<div class="photo-keywords" *ngIf="keywords">
<ng-template ngFor let-keyword [ngForOf]="keywords" let-last="last">
<a *ngIf="searchEnabled"
[routerLink]="['/search', keyword, {type: SearchTypes[SearchTypes.keyword]}]">#{{keyword}}</a>
<span *ngIf="!searchEnabled">#{{keyword}}</span>
<ng-template [ngIf]="!last">, </ng-template>
[routerLink]="['/search', keyword.value, {type: SearchTypes[keyword.type]}]" [ngSwitch]="keyword.type">
<ng-template [ngSwitchCase]="SearchTypes.keyword">#</ng-template><!--
--><ng-template [ngSwitchCase]="SearchTypes.person"><span class="oi oi-person"></span></ng-template><!--
-->{{keyword.value}}</a>
<span *ngIf="!searchEnabled" [ngSwitch]="keyword.type">
<ng-template [ngSwitchCase]="SearchTypes.keyword">#</ng-template><!--
--><ng-template [ngSwitchCase]="SearchTypes.person"><span class="oi oi-person"></span></ng-template><!--
-->{{keyword.value}}</span>
<ng-template [ngIf]="!last">,</ng-template>
</ng-template>
</div>

View File

@ -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 ((<PhotoDTO>this.gridPhoto.media).metadata.caption) {
if ((<PhotoDTO>this.gridPhoto.media).metadata.caption.length > 20) {
return (<PhotoDTO>this.gridPhoto.media).metadata.caption.substring(0, 17) + '...';
}
return (<PhotoDTO>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 {
(<PhotoDTO>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 ((<PhotoDTO>this.gridPhoto.media).metadata.caption) {
if ((<PhotoDTO>this.gridPhoto.media).metadata.caption.length > 20) {
return (<PhotoDTO>this.gridPhoto.media).metadata.caption.substring(0, 17) + '...';
}
return (<PhotoDTO>this.gridPhoto.media).metadata.caption;
}
return this.gridPhoto.media.name;
}
/*
onImageLoad() {
this.loading.show = false;

View File

@ -15,6 +15,7 @@
<span *ngSwitchCase="SearchTypes.video" class="oi oi-video"></span>
<span *ngSwitchCase="SearchTypes.directory" class="oi oi-folder"></span>
<span *ngSwitchCase="SearchTypes.keyword" class="oi oi-tag"></span>
<span *ngSwitchCase="SearchTypes.person" class="oi oi-person"></span>
<span *ngSwitchCase="SearchTypes.position" class="oi oi-map-marker"></span>
</span>
<strong> {{searchResult.searchText}}</strong>

View File

@ -25,6 +25,7 @@
<span *ngSwitchCase="SearchTypes.video" class="oi oi-video"></span>
<span *ngSwitchCase="SearchTypes.directory" class="oi oi-folder"></span>
<span *ngSwitchCase="SearchTypes.keyword" class="oi oi-tag"></span>
<span *ngSwitchCase="SearchTypes.person" class="oi oi-person"></span>
<span *ngSwitchCase="SearchTypes.position" class="oi oi-map-marker"></span>
</span>
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}