From 6b5508c9e6e31605e80b4a5cc6279e274ef619a2 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sat, 17 Nov 2018 19:32:31 +0100 Subject: [PATCH] implementing database support for videos --- backend/model/sql/GalleryManager.ts | 69 ++++++++++-------- backend/model/sql/SQLConnection.ts | 6 ++ backend/model/sql/enitites/DirectoryEntity.ts | 8 +-- backend/model/sql/enitites/MediaEntity.ts | 70 +++++++++++++++++++ backend/model/sql/enitites/PhotoEntity.ts | 55 +++------------ backend/model/sql/enitites/VideoEntity.ts | 21 ++++++ backend/model/threading/DiskMangerWorker.ts | 42 ++++++----- common/entities/MediaDTO.ts | 40 +++++------ common/entities/PhotoDTO.ts | 17 ++++- common/entities/VideoDTO.ts | 13 +++- frontend/app/app.module.ts | 4 +- frontend/app/gallery/gallery.component.ts | 9 +-- frontend/app/gallery/grid/GridMedia.ts | 4 +- .../photo/photo.grid.gallery.component.ts | 9 +-- .../info-panel.lightbox.gallery.component.ts | 17 ++--- .../lightbox/lightbox.gallery.component.html | 6 +- .../lightbox/lightbox.gallery.component.ts | 16 ++--- .../media.lightbox.gallery.component.css} | 0 .../media.lightbox.gallery.component.html} | 0 .../media.lightbox.gallery.component.ts} | 16 ++--- sandbox.ts | 28 -------- test/backend/integration/model/sql/typeorm.ts | 18 ++--- test/backend/unit/model/sql/SearchManager.ts | 4 +- .../model/threading/DiskMangerWorker.spec.ts | 4 +- 24 files changed, 269 insertions(+), 207 deletions(-) create mode 100644 backend/model/sql/enitites/MediaEntity.ts create mode 100644 backend/model/sql/enitites/VideoEntity.ts rename frontend/app/gallery/lightbox/{photo/photo.lightbox.gallery.component.css => media/media.lightbox.gallery.component.css} (100%) rename frontend/app/gallery/lightbox/{photo/photo.lightbox.gallery.component.html => media/media.lightbox.gallery.component.html} (100%) rename frontend/app/gallery/lightbox/{photo/photo.lightbox.gallery.component.ts => media/media.lightbox.gallery.component.ts} (91%) delete mode 100644 sandbox.ts diff --git a/backend/model/sql/GalleryManager.ts b/backend/model/sql/GalleryManager.ts index f7055bf9..a09f2a06 100644 --- a/backend/model/sql/GalleryManager.ts +++ b/backend/model/sql/GalleryManager.ts @@ -13,7 +13,10 @@ import {ISQLGalleryManager} from './IGalleryManager'; import {ReIndexingSensitivity} from '../../../common/config/private/IPrivateConfig'; import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {OrientationType} from '../../../common/entities/RandomQueryDTO'; -import {Brackets} from 'typeorm'; +import {Connection, Brackets} from 'typeorm'; +import {MediaEntity} from './enitites/MediaEntity'; +import {MediaDTO} from '../../../common/entities/MediaDTO'; +import {VideoEntity} from './enitites/VideoEntity'; export class GalleryManager implements IGalleryManager, ISQLGalleryManager { @@ -35,7 +38,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { path: directoryParent }) .leftJoinAndSelect('directory.directories', 'directories') - .leftJoinAndSelect('directory.photos', 'photos') + .leftJoinAndSelect('directory.media', 'media') .getOne(); @@ -63,7 +66,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { if (dir.directories) { for (let i = 0; i < dir.directories.length; i++) { dir.directories[i].media = await connection - .getRepository(PhotoEntity) + .getRepository(MediaEntity) .createQueryBuilder('media') .where('media.directory = :dir', { dir: dir.directories[i].id @@ -132,10 +135,10 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { // saving to db const directoryRepository = connection.getRepository(DirectoryEntity); - const photosRepository = connection.getRepository(PhotoEntity); + const mediaRepository = connection.getRepository(MediaEntity); - let currentDir = await directoryRepository.createQueryBuilder('directory') + let currentDir: DirectoryEntity = await directoryRepository.createQueryBuilder('directory') .where('directory.name = :name AND directory.path = :path', { name: scannedDirectory.name, path: scannedDirectory.path @@ -144,10 +147,18 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { if (!!currentDir) {// Updated parent dir (if it was in the DB previously) currentDir.lastModified = scannedDirectory.lastModified; currentDir.lastScanned = scannedDirectory.lastScanned; + const media: MediaEntity[] = currentDir.media; + delete currentDir.media; currentDir = await directoryRepository.save(currentDir); + media.forEach(m => m.directory = currentDir); + currentDir.media = await this.saveMedia(connection, media); } else { + const media = scannedDirectory.media; + delete scannedDirectory.media; (scannedDirectory).lastScanned = scannedDirectory.lastScanned; currentDir = await directoryRepository.save(scannedDirectory); + media.forEach(m => m.directory = currentDir); + currentDir.media = await this.saveMedia(connection, media); } const childDirectories = await directoryRepository.createQueryBuilder('directory') @@ -180,7 +191,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { scannedDirectory.directories[i].media[j].directory = d; } - await photosRepository.save(scannedDirectory.directories[i].media); + await this.saveMedia(connection, scannedDirectory.directories[i].media); } } @@ -188,53 +199,49 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { await directoryRepository.remove(childDirectories); - const indexedPhotos = await photosRepository.createQueryBuilder('media') + const indexedMedia = await mediaRepository.createQueryBuilder('media') .where('media.directory = :dir', { dir: currentDir.id }).getMany(); - const photosToSave = []; + const mediaToSave = []; for (let i = 0; i < scannedDirectory.media.length; i++) { - let photo = null; - for (let j = 0; j < indexedPhotos.length; j++) { - if (indexedPhotos[j].name === scannedDirectory.media[i].name) { - photo = indexedPhotos[j]; - indexedPhotos.splice(j, 1); + let media = null; + for (let j = 0; j < indexedMedia.length; j++) { + if (indexedMedia[j].name === scannedDirectory.media[i].name) { + media = indexedMedia[j]; + indexedMedia.splice(j, 1); break; } } - if (photo == null) { + if (media == null) { scannedDirectory.media[i].directory = null; - photo = Utils.clone(scannedDirectory.media[i]); + media = Utils.clone(scannedDirectory.media[i]); scannedDirectory.media[i].directory = scannedDirectory; - photo.directory = currentDir; + media.directory = currentDir; } - if (photo.metadata.keywords !== scannedDirectory.media[i].metadata.keywords || - photo.metadata.cameraData !== (scannedDirectory.media[i]).metadata.cameraData || - photo.metadata.positionData !== scannedDirectory.media[i].metadata.positionData || - photo.metadata.size !== scannedDirectory.media[i].metadata.size) { - photo.metadata.keywords = scannedDirectory.media[i].metadata.keywords; - photo.metadata.cameraData = (scannedDirectory.media[i]).metadata.cameraData; - photo.metadata.positionData = scannedDirectory.media[i].metadata.positionData; - photo.metadata.size = scannedDirectory.media[i].metadata.size; - photosToSave.push(photo); + if (!Utils.equalsFilter(media.metadata, scannedDirectory.media[i].metadata)) { + media.metadata = (scannedDirectory.media[i]).metadata; + mediaToSave.push(media); } } - await photosRepository.save(photosToSave); - await photosRepository.remove(indexedPhotos); - + await this.saveMedia(connection, mediaToSave); + await mediaRepository.remove(indexedMedia); + } + private async saveMedia(connection: Connection, mediaList: MediaDTO[]): Promise { + const list = await connection.getRepository(VideoEntity).save(mediaList.filter(m => MediaDTO.isVideo(m))); + return list.concat(await connection.getRepository(PhotoEntity).save(mediaList.filter(m => MediaDTO.isPhoto(m)))); } async getRandomPhoto(queryFilter: RandomQuery): Promise { const connection = await SQLConnection.getConnection(); const photosRepository = connection.getRepository(PhotoEntity); - - const query = photosRepository.createQueryBuilder('media'); - query.innerJoinAndSelect('media.directory', 'directory'); + const query = photosRepository.createQueryBuilder('photo'); + query.innerJoinAndSelect('photo.directory', 'directory'); if (queryFilter.directory) { const directoryName = path.basename(queryFilter.directory); diff --git a/backend/model/sql/SQLConnection.ts b/backend/model/sql/SQLConnection.ts index 3eb151ed..4bcedd91 100644 --- a/backend/model/sql/SQLConnection.ts +++ b/backend/model/sql/SQLConnection.ts @@ -11,6 +11,8 @@ import {PasswordHelper} from '../PasswordHelper'; import {ProjectPath} from '../../ProjectPath'; import {VersionEntity} from './enitites/VersionEntity'; import {Logger} from '../../Logger'; +import {MediaEntity} from './enitites/MediaEntity'; +import {VideoEntity} from './enitites/VideoEntity'; export class SQLConnection { @@ -30,7 +32,9 @@ export class SQLConnection { options.name = 'main'; options.entities = [ UserEntity, + MediaEntity, PhotoEntity, + VideoEntity, DirectoryEntity, SharingEntity, VersionEntity @@ -53,7 +57,9 @@ export class SQLConnection { options.name = 'test'; options.entities = [ UserEntity, + MediaEntity, PhotoEntity, + VideoEntity, DirectoryEntity, SharingEntity, VersionEntity diff --git a/backend/model/sql/enitites/DirectoryEntity.ts b/backend/model/sql/enitites/DirectoryEntity.ts index ceeb3860..29e8d953 100644 --- a/backend/model/sql/enitites/DirectoryEntity.ts +++ b/backend/model/sql/enitites/DirectoryEntity.ts @@ -1,6 +1,6 @@ import {Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn} from 'typeorm'; import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO'; -import {PhotoEntity} from './PhotoEntity'; +import {MediaEntity} from './MediaEntity'; @Entity() export class DirectoryEntity implements DirectoryDTO { @@ -32,9 +32,9 @@ export class DirectoryEntity implements DirectoryDTO { public parent: DirectoryEntity; @OneToMany(type => DirectoryEntity, dir => dir.parent) - public directories: Array; + public directories: DirectoryEntity[]; - @OneToMany(type => PhotoEntity, photo => photo.directory) - public media: Array; + @OneToMany(type => MediaEntity, media => media.directory) + public media: MediaEntity[]; } diff --git a/backend/model/sql/enitites/MediaEntity.ts b/backend/model/sql/enitites/MediaEntity.ts new file mode 100644 index 00000000..13cdcfb7 --- /dev/null +++ b/backend/model/sql/enitites/MediaEntity.ts @@ -0,0 +1,70 @@ +import {Column, Entity, ManyToOne, PrimaryGeneratedColumn, TableInheritance} from 'typeorm'; +import {DirectoryEntity} from './DirectoryEntity'; +import {MediaDimension, MediaDTO, MediaMetadata} from '../../../../common/entities/MediaDTO'; +import {OrientationTypes} from 'ts-exif-parser'; +import {CameraMetadataEntity, PositionMetaDataEntity} from './PhotoEntity'; + + +export class MediaDimensionEntity implements MediaDimension { + + @Column('int') + width: number; + + @Column('int') + height: number; +} + + +export class MediaMetadataEntity implements MediaMetadata { + + @Column(type => MediaDimensionEntity) + size: MediaDimensionEntity; + + @Column('bigint') + creationDate: number; + + @Column('int') + fileSize: number; + + + @Column('simple-array') + keywords: string[]; + + @Column(type => CameraMetadataEntity) + cameraData: CameraMetadataEntity; + + @Column(type => PositionMetaDataEntity) + positionData: PositionMetaDataEntity; + + @Column('tinyint', {default: OrientationTypes.TOP_LEFT}) + orientation: OrientationTypes; + + @Column('int') + bitRate: number; + + @Column('bigint') + duration: number; +} + +// TODO: fix inheritance once its working in typeorm +@Entity() +@TableInheritance({column: {type: 'varchar', name: 'type'}}) +export abstract class MediaEntity implements MediaDTO { + + @PrimaryGeneratedColumn() + id: number; + + @Column('text') + name: string; + + @ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE'}) + directory: DirectoryEntity; + + @Column(type => MediaMetadataEntity) + metadata: MediaMetadataEntity; + + readyThumbnails: number[] = []; + + readyIcon = false; + +} diff --git a/backend/model/sql/enitites/PhotoEntity.ts b/backend/model/sql/enitites/PhotoEntity.ts index 04f5d330..7e0ce869 100644 --- a/backend/model/sql/enitites/PhotoEntity.ts +++ b/backend/model/sql/enitites/PhotoEntity.ts @@ -1,13 +1,11 @@ -import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm'; -import {CameraMetadata, PhotoDTO, PhotoMetadata} from '../../../../common/entities/PhotoDTO'; -import {DirectoryEntity} from './DirectoryEntity'; +import {Column, Entity, ChildEntity} from 'typeorm'; +import {CameraMetadata, GPSMetadata, PhotoDTO, PhotoMetadata, PositionMetaData} from '../../../../common/entities/PhotoDTO'; import {OrientationTypes} from 'ts-exif-parser'; -import {GPSMetadata, MediaDimension, PositionMetaData} from '../../../../common/entities/MediaDTO'; +import {MediaEntity, MediaMetadataEntity} from './MediaEntity'; -@Entity() export class CameraMetadataEntity implements CameraMetadata { - @Column('text', {nullable: true}) + @Column('int', {nullable: true}) ISO: number; @Column('text', {nullable: true}) @@ -30,7 +28,6 @@ export class CameraMetadataEntity implements CameraMetadata { } -@Entity() export class GPSMetadataEntity implements GPSMetadata { @Column('int', {nullable: true}) @@ -41,18 +38,6 @@ export class GPSMetadataEntity implements GPSMetadata { altitude: number; } -@Entity() -export class ImageSizeEntity implements MediaDimension { - - @Column('int') - width: number; - - @Column('int') - height: number; -} - - -@Entity() export class PositionMetaDataEntity implements PositionMetaData { @Column(type => GPSMetadataEntity) @@ -69,11 +54,10 @@ export class PositionMetaDataEntity implements PositionMetaData { } -@Entity() -export class PhotoMetadataEntity implements PhotoMetadata { +export class PhotoMetadataEntity extends MediaMetadataEntity implements PhotoMetadata { @Column('simple-array') - keywords: Array; + keywords: string[]; @Column(type => CameraMetadataEntity) cameraData: CameraMetadataEntity; @@ -84,34 +68,11 @@ export class PhotoMetadataEntity implements PhotoMetadata { @Column('tinyint', {default: OrientationTypes.TOP_LEFT}) orientation: OrientationTypes; - @Column(type => ImageSizeEntity) - size: ImageSizeEntity; - - @Column('bigint') - creationDate: number; - - @Column('int') - fileSize: number; } -@Entity() -export class PhotoEntity implements PhotoDTO { - - @PrimaryGeneratedColumn() - id: number; - - @Column('text') - name: string; - - @ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE'}) - directory: DirectoryEntity; - +@ChildEntity() +export class PhotoEntity extends MediaEntity implements PhotoDTO { @Column(type => PhotoMetadataEntity) metadata: PhotoMetadataEntity; - - readyThumbnails: Array = []; - - readyIcon = false; - } diff --git a/backend/model/sql/enitites/VideoEntity.ts b/backend/model/sql/enitites/VideoEntity.ts new file mode 100644 index 00000000..3c9d0461 --- /dev/null +++ b/backend/model/sql/enitites/VideoEntity.ts @@ -0,0 +1,21 @@ +import {Column, Entity, ChildEntity} from 'typeorm'; +import { MediaEntity, MediaMetadataEntity} from './MediaEntity'; +import {VideoDTO, VideoMetadata} from '../../../../common/entities/VideoDTO'; + + +export class VideoMetadataEntity extends MediaMetadataEntity implements VideoMetadata { + + @Column('int') + bitRate: number; + + @Column('bigint') + duration: number; + +} + + +@ChildEntity() +export class VideoEntity extends MediaEntity implements VideoDTO { + @Column(type => VideoMetadataEntity) + metadata: VideoMetadataEntity; +} diff --git a/backend/model/threading/DiskMangerWorker.ts b/backend/model/threading/DiskMangerWorker.ts index bced1b90..7094ba99 100644 --- a/backend/model/threading/DiskMangerWorker.ts +++ b/backend/model/threading/DiskMangerWorker.ts @@ -1,16 +1,16 @@ import * as fs from 'fs'; import * as path from 'path'; import {DirectoryDTO} from '../../../common/entities/DirectoryDTO'; -import {CameraMetadata, PhotoDTO, PhotoMetadata} from '../../../common/entities/PhotoDTO'; +import {CameraMetadata, GPSMetadata, PhotoDTO, PhotoMetadata} from '../../../common/entities/PhotoDTO'; import {Logger} from '../../Logger'; import {IptcParser} from 'ts-node-iptc'; import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser'; import * as ffmpeg from 'fluent-ffmpeg'; -import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg'; +import {FfprobeData} from 'fluent-ffmpeg'; import {ProjectPath} from '../../ProjectPath'; import {Config} from '../../../common/config/private/Config'; -import {VideoDTO} from '../../../common/entities/VideoDTO'; -import {GPSMetadata, MediaDimension, MediaMetadata} from '../../../common/entities/MediaDTO'; +import {VideoDTO, VideoMetadata} from '../../../common/entities/VideoDTO'; +import {MediaDimension, MediaMetadata} from '../../../common/entities/MediaDTO'; const LOG_TAG = '[DiskManagerTask]'; @@ -87,7 +87,7 @@ export class DiskMangerWorker { directory.media.push({ name: file, directory: null, - metadata: await DiskMangerWorker.loadPVideoMetadata(fullFilePath) + metadata: await DiskMangerWorker.loadVideoMetadata(fullFilePath) }); if (maxPhotos != null && directory.media.length > maxPhotos) { @@ -106,16 +106,15 @@ export class DiskMangerWorker { } - private static loadPVideoMetadata(fullPath: string): Promise { - return new Promise((resolve, reject) => { - const metadata: MediaMetadata = { - keywords: [], - positionData: null, + public static loadVideoMetadata(fullPath: string): Promise { + return new Promise((resolve, reject) => { + const metadata: VideoMetadata = { size: { width: 0, height: 0 }, - orientation: OrientationTypes.TOP_LEFT, + bitRate: 0, + duration: 0, creationDate: 0, fileSize: 0 }; @@ -129,13 +128,20 @@ export class DiskMangerWorker { return reject(err); } - metadata.size = { - width: data.streams[0].width, - height: data.streams[0].height - }; + if (!data.streams[0]) { + return resolve(metadata); + } try { - metadata.creationDate = data.streams[0].tags.creation_time; + + metadata.size = { + width: data.streams[0].width, + height: data.streams[0].height + }; + + metadata.duration = Math.floor(data.streams[0].duration * 1000); + metadata.bitRate = data.streams[0].bit_rate; + metadata.creationDate = Date.parse(data.streams[0].tags.creation_time); } catch (err) { } @@ -144,7 +150,7 @@ export class DiskMangerWorker { }); } - private static loadPhotoMetadata(fullPath: string): Promise { + public static loadPhotoMetadata(fullPath: string): Promise { return new Promise((resolve, reject) => { fs.readFile(fullPath, (err, data) => { if (err) { @@ -220,7 +226,7 @@ export class DiskMangerWorker { 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/MediaDTO.ts b/common/entities/MediaDTO.ts index 0520fbf3..9d092f7f 100644 --- a/common/entities/MediaDTO.ts +++ b/common/entities/MediaDTO.ts @@ -1,6 +1,7 @@ import {DirectoryDTO} from './DirectoryDTO'; import {PhotoDTO} from './PhotoDTO'; import {OrientationTypes} from 'ts-exif-parser'; +import {VideoDTO} from './VideoDTO'; export interface MediaDTO { id: number; @@ -13,27 +14,12 @@ export interface MediaDTO { export interface MediaMetadata { - keywords: string[]; - positionData: PositionMetaData; size: MediaDimension; creationDate: number; fileSize: number; } -export interface PositionMetaData { - GPSData?: GPSMetadata; - country?: string; - state?: string; - city?: string; -} - -export interface GPSMetadata { - latitude?: number; - longitude?: number; - altitude?: number; -} - export interface MediaDimension { width: number; height: number; @@ -41,14 +27,14 @@ export interface MediaDimension { export module MediaDTO { export const hasPositionData = (media: MediaDTO): boolean => { - return !!media.metadata.positionData && - !!(media.metadata.positionData.city || - media.metadata.positionData.state || - media.metadata.positionData.country || - (media.metadata.positionData.GPSData && - media.metadata.positionData.GPSData.altitude && - media.metadata.positionData.GPSData.latitude && - media.metadata.positionData.GPSData.longitude)); + return !!(media).metadata.positionData && + !!((media).metadata.positionData.city || + (media).metadata.positionData.state || + (media).metadata.positionData.country || + ((media).metadata.positionData.GPSData && + (media).metadata.positionData.GPSData.altitude && + (media).metadata.positionData.GPSData.latitude && + (media).metadata.positionData.GPSData.longitude)); }; export const isSideWay = (media: MediaDTO): boolean => { @@ -63,6 +49,14 @@ export module MediaDTO { }; + export const isPhoto = (media: MediaDTO): boolean => { + return typeof (media).metadata.keywords !== 'undefined' && (media).metadata.keywords !== null; + }; + + export const isVideo = (media: MediaDTO): boolean => { + return !MediaDTO.isPhoto(media); + }; + export const getRotatedSize = (photo: MediaDTO): MediaDimension => { if (isSideWay(photo)) { // noinspection JSSuspiciousNameCombination diff --git a/common/entities/PhotoDTO.ts b/common/entities/PhotoDTO.ts index d9e798da..3b2a05b2 100644 --- a/common/entities/PhotoDTO.ts +++ b/common/entities/PhotoDTO.ts @@ -1,6 +1,6 @@ import {DirectoryDTO} from './DirectoryDTO'; import {OrientationTypes} from 'ts-exif-parser'; -import {MediaDTO, MediaMetadata, MediaDimension, PositionMetaData} from './MediaDTO'; +import {MediaDTO, MediaMetadata, MediaDimension} from './MediaDTO'; export interface PhotoDTO extends MediaDTO { id: number; @@ -21,6 +21,21 @@ export interface PhotoMetadata extends MediaMetadata { fileSize: number; } + +export interface PositionMetaData { + GPSData?: GPSMetadata; + country?: string; + state?: string; + city?: string; +} + +export interface GPSMetadata { + latitude?: number; + longitude?: number; + altitude?: number; +} + + export interface CameraMetadata { ISO?: number; model?: string; diff --git a/common/entities/VideoDTO.ts b/common/entities/VideoDTO.ts index b4258d1b..e28658c5 100644 --- a/common/entities/VideoDTO.ts +++ b/common/entities/VideoDTO.ts @@ -1,9 +1,18 @@ import {DirectoryDTO} from './DirectoryDTO'; -import {MediaDTO, MediaMetadata} from './MediaDTO'; +import {MediaDimension, MediaDTO, MediaMetadata} from './MediaDTO'; export interface VideoDTO extends MediaDTO { id: number; name: string; directory: DirectoryDTO; - metadata: MediaMetadata; + metadata: VideoMetadata; +} + + +export interface VideoMetadata extends MediaMetadata { + size: MediaDimension; + creationDate: number; + bitRate: number; + duration: number; // in milliseconds + fileSize: number; } diff --git a/frontend/app/app.module.ts b/frontend/app/app.module.ts index 35156764..a21509aa 100644 --- a/frontend/app/app.module.ts +++ b/frontend/app/app.module.ts @@ -20,7 +20,7 @@ import {FullScreenService} from './gallery/fullscreen.service'; import {AuthenticationService} from './model/network/authentication.service'; import {UserMangerSettingsComponent} from './settings/usermanager/usermanager.settings.component'; import {FrameComponent} from './frame/frame.component'; -import {GalleryLightboxPhotoComponent} from './gallery/lightbox/photo/photo.lightbox.gallery.component'; +import {GalleryLightboxMediaComponent} from './gallery/lightbox/media/media.lightbox.gallery.component'; import {GalleryPhotoLoadingComponent} from './gallery/grid/photo/loading/loading.photo.grid.gallery.component'; import {GalleryNavigatorComponent} from './gallery/navigator/navigator.gallery.component'; import {GallerySearchComponent} from './gallery/search/search.gallery.component'; @@ -139,7 +139,7 @@ export function translationsFactory(locale: string) { FrameComponent, LanguageComponent, // Gallery - GalleryLightboxPhotoComponent, + GalleryLightboxMediaComponent, GalleryPhotoLoadingComponent, GalleryGridComponent, GalleryDirectoryComponent, diff --git a/frontend/app/gallery/gallery.component.ts b/frontend/app/gallery/gallery.component.ts index fdab04d7..8ba2ff5e 100644 --- a/frontend/app/gallery/gallery.component.ts +++ b/frontend/app/gallery/gallery.component.ts @@ -127,11 +127,12 @@ export class GalleryComponent implements OnInit, OnDestroy { this.directories = tmp.directories; this.sortDirectories(); this.isPhotoWithLocation = false; + for (let i = 0; i < tmp.media.length; i++) { - if (tmp.media[i].metadata && - tmp.media[i].metadata.positionData && - tmp.media[i].metadata.positionData.GPSData && - tmp.media[i].metadata.positionData.GPSData.longitude + if ((tmp.media[i]).metadata && + (tmp.media[i]).metadata.positionData && + (tmp.media[i]).metadata.positionData.GPSData && + (tmp.media[i]).metadata.positionData.GPSData.longitude ) { this.isPhotoWithLocation = true; break; diff --git a/frontend/app/gallery/grid/GridMedia.ts b/frontend/app/gallery/grid/GridMedia.ts index 098588c5..48f92a50 100644 --- a/frontend/app/gallery/grid/GridMedia.ts +++ b/frontend/app/gallery/grid/GridMedia.ts @@ -15,11 +15,11 @@ export class GridMedia extends Media { } isPhoto(): boolean { - return typeof (this.media).metadata.cameraData !== 'undefined'; + return MediaDTO.isPhoto(this.media); } isVideo(): boolean { - return typeof (this.media).metadata.cameraData === 'undefined'; + return MediaDTO.isVideo(this.media); } 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 ddb8f0ef..61c9badd 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts @@ -7,6 +7,7 @@ import {Thumbnail, ThumbnailManagerService} from '../../thumnailManager.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'; @Component({ selector: 'app-gallery-grid-photo', @@ -75,12 +76,12 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { getPositionText(): string { - if (!this.gridPhoto) { + if (!this.gridPhoto || !this.gridPhoto.isPhoto()) { return ''; } - return this.gridPhoto.media.metadata.positionData.city || - this.gridPhoto.media.metadata.positionData.state || - this.gridPhoto.media.metadata.positionData.country; + return (this.gridPhoto.media).metadata.positionData.city || + (this.gridPhoto.media).metadata.positionData.state || + (this.gridPhoto.media).metadata.positionData.country; } diff --git a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts index 30782017..52bc656f 100644 --- a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts @@ -10,7 +10,8 @@ import {MediaDTO} from '../../../../../common/entities/MediaDTO'; }) export class InfoPanelLightboxComponent { @Input() media: MediaDTO; - @Output('onClose') onClose = new EventEmitter(); + @Output() closed = new EventEmitter(); + public mapEnabled = true; constructor(public elementRef: ElementRef) { @@ -56,21 +57,21 @@ export class InfoPanelLightboxComponent { } hasGPS() { - return this.media.metadata.positionData && this.media.metadata.positionData.GPSData && - this.media.metadata.positionData.GPSData.latitude && this.media.metadata.positionData.GPSData.longitude; + return (this.media).metadata.positionData && (this.media).metadata.positionData.GPSData && + (this.media).metadata.positionData.GPSData.latitude && (this.media).metadata.positionData.GPSData.longitude; } getPositionText(): string { - if (!this.media.metadata.positionData) { + if (!(this.media).metadata.positionData) { return ''; } - let str = this.media.metadata.positionData.city || - this.media.metadata.positionData.state || ''; + let str = (this.media).metadata.positionData.city || + (this.media).metadata.positionData.state || ''; if (str.length !== 0) { str += ', '; } - str += this.media.metadata.positionData.country || ''; + str += (this.media).metadata.positionData.country || ''; return str; } @@ -80,7 +81,7 @@ export class InfoPanelLightboxComponent { } close() { - this.onClose.emit(); + this.closed.emit(); } } diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.html b/frontend/app/gallery/lightbox/lightbox.gallery.component.html index 2c821733..a8da1740 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.html +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.html @@ -5,11 +5,11 @@
+ (closed)="hideInfoPanel()">
diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts index 8a709e06..fb7f5804 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts @@ -16,7 +16,7 @@ import {Dimension} from '../../model/IRenderable'; import {FullScreenService} from '../fullscreen.service'; import {OverlayService} from '../overlay.service'; import {animate, AnimationBuilder, AnimationPlayer, style} from '@angular/animations'; -import {GalleryLightboxPhotoComponent} from './photo/photo.lightbox.gallery.component'; +import {GalleryLightboxMediaComponent} from './media/media.lightbox.gallery.component'; import {Observable, Subscription, timer} from 'rxjs'; import {filter} from 'rxjs/operators'; import {ActivatedRoute, Params, Router} from '@angular/router'; @@ -37,7 +37,7 @@ export enum LightboxStates { }) export class GalleryLightboxComponent implements OnDestroy, OnInit { - @ViewChild('photo') photoElement: GalleryLightboxPhotoComponent; + @ViewChild('photo') mediaElement: GalleryLightboxMediaComponent; @ViewChild('lightbox') lightboxElement: ElementRef; public navigation = {hasPrev: true, hasNext: true}; @@ -245,7 +245,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { break; case ' ': // space if (this.activePhoto && this.activePhoto.gridPhoto.isVideo()) { - this.photoElement.playPause(); + this.mediaElement.playPause(); } break; } @@ -292,7 +292,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { animate(300, style(Dimension.toString(to))) ]) - .create(this.photoElement.elementRef.nativeElement); + .create(this.mediaElement.elementRef.nativeElement); elem.play(); return elem; } @@ -356,12 +356,12 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { public play() { this.pause(); this.timerSub = this.timer.pipe(filter(t => t % 2 === 0)).subscribe(() => { - if (this.photoElement.imageLoadFinished === false) { + if (this.mediaElement.imageLoadFinished === false) { return; } // do not skip video if its playing if (this.activePhoto && this.activePhoto.gridPhoto.isVideo() && - !this.photoElement.Paused) { + !this.mediaElement.Paused) { return; } if (this.navigation.hasNext) { @@ -400,11 +400,11 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { public fastForward() { this.pause(); this.timerSub = this.timer.subscribe(() => { - if (this.photoElement.imageLoadFinished === false) { + if (this.mediaElement.imageLoadFinished === false) { return; } if (this.activePhoto && this.activePhoto.gridPhoto.isVideo() && - !this.photoElement.Paused) { + !this.mediaElement.Paused) { return; } if (this.navigation.hasNext) { diff --git a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.css b/frontend/app/gallery/lightbox/media/media.lightbox.gallery.component.css similarity index 100% rename from frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.css rename to frontend/app/gallery/lightbox/media/media.lightbox.gallery.component.css diff --git a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.html b/frontend/app/gallery/lightbox/media/media.lightbox.gallery.component.html similarity index 100% rename from frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.html rename to frontend/app/gallery/lightbox/media/media.lightbox.gallery.component.html diff --git a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/media/media.lightbox.gallery.component.ts similarity index 91% rename from frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts rename to frontend/app/gallery/lightbox/media/media.lightbox.gallery.component.ts index 7c18e5f5..b7b30608 100644 --- a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/media/media.lightbox.gallery.component.ts @@ -1,15 +1,14 @@ import {Component, ElementRef, Input, Output, OnChanges, ViewChild} from '@angular/core'; import {GridMedia} from '../../grid/GridMedia'; -import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; import {FixOrientationPipe} from '../../FixOrientationPipe'; import {MediaDTO} from '../../../../../common/entities/MediaDTO'; @Component({ - selector: 'app-gallery-lightbox-photo', - styleUrls: ['./photo.lightbox.gallery.component.css'], - templateUrl: './photo.lightbox.gallery.component.html' + selector: 'app-gallery-lightbox-media', + styleUrls: ['./media.lightbox.gallery.component.css'], + templateUrl: './media.lightbox.gallery.component.html' }) -export class GalleryLightboxPhotoComponent implements OnChanges { +export class GalleryLightboxMediaComponent implements OnChanges { @Input() gridMedia: GridMedia; @Input() loadMedia = false; @@ -26,7 +25,7 @@ export class GalleryLightboxPhotoComponent implements OnChanges { thumbnailSrc: string = null; photoSrc: string = null; - private videoProgress: number = 0; + private videoProgress = 0; constructor(public elementRef: ElementRef) { } @@ -49,10 +48,9 @@ export class GalleryLightboxPhotoComponent implements OnChanges { FixOrientationPipe.transform(this.gridMedia.getPhotoPath(), this.gridMedia.Orientation) .then((src) => this.photoSrc = src); } - - } + /** Video **/ private onVideoProgress() { this.videoProgress = (100 / this.video.nativeElement.duration) * this.video.nativeElement.currentTime; } @@ -124,7 +122,7 @@ export class GalleryLightboxPhotoComponent implements OnChanges { onImageError() { // TODO:handle error this.imageLoadFinished = true; - console.error('Error: cannot load image for lightbox url: ' + this.gridMedia.getPhotoPath()); + console.error('Error: cannot load media for lightbox url: ' + this.gridMedia.getPhotoPath()); } diff --git a/sandbox.ts b/sandbox.ts deleted file mode 100644 index 4a68313e..00000000 --- a/sandbox.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as FfmpegCommand from 'fluent-ffmpeg'; - - -const run = async () => { - - const command = FfmpegCommand('demo/images/fulbright_mediumbr.mp4'); - // command.setFfmpegPath('ffmpeg/ffmpeg.exe'); - // command.setFfprobePath('ffmpeg/ffprobe.exe'); - // command.setFlvtoolPath('ffmpeg/ffplay.exe'); - FfmpegCommand('demo/images/fulbright_mediumbr.mp4').ffprobe((err,data) => { - console.log(data); - }); - command // setup event handlers - .on('filenames', function (filenames) { - console.log('screenshots are ' + filenames.join(', ')); - }) - .on('end', function () { - console.log('screenshots were saved'); - }) - .on('error', function (err) { - console.log('an error happened: ' + err.message); - }) - .outputOptions(['-qscale:v 4']) - // take 2 screenshots at predefined timemarks and size - .takeScreenshots({timemarks: ['10%'], size: '450x?', filename: 'thumbnail2-at-%s-seconds.jpg'}); -}; - -run(); diff --git a/test/backend/integration/model/sql/typeorm.ts b/test/backend/integration/model/sql/typeorm.ts index 0da0b774..f3cc6279 100644 --- a/test/backend/integration/model/sql/typeorm.ts +++ b/test/backend/integration/model/sql/typeorm.ts @@ -11,11 +11,11 @@ import {DirectoryEntity} from '../../../../../backend/model/sql/enitites/Directo import { CameraMetadataEntity, GPSMetadataEntity, - ImageSizeEntity, PhotoEntity, PhotoMetadataEntity, PositionMetaDataEntity } from '../../../../../backend/model/sql/enitites/PhotoEntity'; +import {MediaDimensionEntity} from '../../../../../backend/model/sql/enitites/MediaEntity'; describe('Typeorm integration', () => { @@ -68,7 +68,7 @@ describe('Typeorm integration', () => { const getPhoto = () => { - const sd = new ImageSizeEntity(); + const sd = new MediaDimensionEntity(); sd.height = 200; sd.width = 200; const gps = new GPSMetadataEntity(); @@ -146,7 +146,7 @@ describe('Typeorm integration', () => { const conn = await SQLConnection.getConnection(); const pr = conn.getRepository(PhotoEntity); const dir = await conn.getRepository(DirectoryEntity).save(getDir()); - let photo = getPhoto(); + const photo = getPhoto(); photo.directory = dir; await pr.save(photo); expect((await pr.find()).length).to.equal(1); @@ -156,11 +156,11 @@ describe('Typeorm integration', () => { const conn = await SQLConnection.getConnection(); const pr = conn.getRepository(PhotoEntity); const dir = await conn.getRepository(DirectoryEntity).save(getDir()); - let photo = getPhoto(); + const photo = getPhoto(); photo.directory = dir; await pr.save(photo); - let photos = await pr + const photos = await pr .createQueryBuilder('media') .orderBy('media.metadata.creationDate', 'ASC') .where('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + photo.metadata.positionData.city + '%'}) @@ -177,12 +177,12 @@ describe('Typeorm integration', () => { const conn = await SQLConnection.getConnection(); const pr = conn.getRepository(PhotoEntity); const dir = await conn.getRepository(DirectoryEntity).save(getDir()); - let photo = getPhoto(); + const photo = getPhoto(); photo.directory = dir; const city = photo.metadata.positionData.city; photo.metadata.positionData = null; await pr.save(photo); - let photos = await pr + const photos = await pr .createQueryBuilder('media') .orderBy('media.metadata.creationDate', 'ASC') .where('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + city + '%'}) @@ -196,10 +196,10 @@ describe('Typeorm integration', () => { it('should open and close connection twice with media added ', async () => { let conn = await SQLConnection.getConnection(); const dir = await conn.getRepository(DirectoryEntity).save(getDir()); - let dir2 = getDir(); + const dir2 = getDir(); dir2.parent = dir; await conn.getRepository(DirectoryEntity).save(dir2); - let photo = getPhoto(); + const photo = getPhoto(); photo.directory = dir2; await await conn.getRepository(PhotoEntity).save(photo); await SQLConnection.close(); diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index 56c9e8a6..b16f31e5 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -7,7 +7,6 @@ import {SQLConnection} from '../../../../../backend/model/sql/SQLConnection'; import { CameraMetadataEntity, GPSMetadataEntity, - ImageSizeEntity, PhotoEntity, PhotoMetadataEntity, PositionMetaDataEntity @@ -16,6 +15,7 @@ import {SearchManager} from '../../../../../backend/model/sql/SearchManager'; import {AutoCompleteItem, SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO'; import {DirectoryEntity} from '../../../../../backend/model/sql/enitites/DirectoryEntity'; +import {MediaDimensionEntity} from '../../../../../backend/model/sql/enitites/MediaEntity'; describe('SearchManager', () => { @@ -30,7 +30,7 @@ describe('SearchManager', () => { dir.lastScanned = null; const getPhoto = () => { - const sd = new ImageSizeEntity(); + const sd = new MediaDimensionEntity(); sd.height = 200; sd.width = 200; const gps = new GPSMetadataEntity(); diff --git a/test/backend/unit/model/threading/DiskMangerWorker.spec.ts b/test/backend/unit/model/threading/DiskMangerWorker.spec.ts index 474b449a..a4f3682c 100644 --- a/test/backend/unit/model/threading/DiskMangerWorker.spec.ts +++ b/test/backend/unit/model/threading/DiskMangerWorker.spec.ts @@ -13,7 +13,7 @@ describe('DiskMangerWorker', () => { const dir = await DiskMangerWorker.scanDirectory('/'); expect(dir.media.length).to.be.equals(1); expect(dir.media[0].name).to.be.equals('test image öüóőúéáű-.,.jpg'); - expect(dir.media[0].metadata.keywords).to.deep.equals(['Berkley', 'USA', 'űáéúőóüö ŰÁÉÚŐÓÜÖ']); + expect((dir.media[0]).metadata.keywords).to.deep.equals(['Berkley', 'USA', 'űáéúőóüö ŰÁÉÚŐÓÜÖ']); expect(dir.media[0].metadata.fileSize).to.deep.equals(62392); expect(dir.media[0].metadata.size).to.deep.equals({width: 140, height: 93}); expect((dir.media[0]).metadata.cameraData).to.deep.equals({ @@ -26,7 +26,7 @@ describe('DiskMangerWorker', () => { lens: 'EF-S15-85mm f/3.5-5.6 IS USM' }); - expect(dir.media[0].metadata.positionData).to.deep.equals({ + expect((dir.media[0]).metadata.positionData).to.deep.equals({ GPSData: { latitude: 37.871093333333334, longitude: -122.25678,