mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
implementing database support for videos
This commit is contained in:
parent
f4df4a638c
commit
6b5508c9e6
@ -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;
|
||||
(<DirectoryEntity>scannedDirectory).lastScanned = scannedDirectory.lastScanned;
|
||||
currentDir = await directoryRepository.save(<DirectoryEntity>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 !== (<PhotoDTO>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 = (<PhotoDTO>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 = (<PhotoDTO>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<MediaEntity[]> {
|
||||
const list = await connection.getRepository(VideoEntity).save(<VideoEntity[]>mediaList.filter(m => MediaDTO.isVideo(m)));
|
||||
return list.concat(await connection.getRepository(PhotoEntity).save(<PhotoEntity[]>mediaList.filter(m => MediaDTO.isPhoto(m))));
|
||||
}
|
||||
|
||||
async getRandomPhoto(queryFilter: RandomQuery): Promise<PhotoDTO> {
|
||||
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);
|
||||
|
@ -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
|
||||
|
@ -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<DirectoryEntity>;
|
||||
public directories: DirectoryEntity[];
|
||||
|
||||
@OneToMany(type => PhotoEntity, photo => photo.directory)
|
||||
public media: Array<PhotoEntity>;
|
||||
@OneToMany(type => MediaEntity, media => media.directory)
|
||||
public media: MediaEntity[];
|
||||
|
||||
}
|
||||
|
70
backend/model/sql/enitites/MediaEntity.ts
Normal file
70
backend/model/sql/enitites/MediaEntity.ts
Normal file
@ -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;
|
||||
|
||||
}
|
@ -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<string>;
|
||||
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<number> = [];
|
||||
|
||||
readyIcon = false;
|
||||
|
||||
}
|
||||
|
21
backend/model/sql/enitites/VideoEntity.ts
Normal file
21
backend/model/sql/enitites/VideoEntity.ts
Normal file
@ -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;
|
||||
}
|
@ -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(<VideoDTO>{
|
||||
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<MediaMetadata> {
|
||||
return new Promise<MediaMetadata>((resolve, reject) => {
|
||||
const metadata: MediaMetadata = <MediaMetadata>{
|
||||
keywords: [],
|
||||
positionData: null,
|
||||
public static loadVideoMetadata(fullPath: string): Promise<VideoMetadata> {
|
||||
return new Promise<VideoMetadata>((resolve, reject) => {
|
||||
const metadata: VideoMetadata = <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<PhotoMetadata> {
|
||||
public static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
||||
return new Promise<PhotoMetadata>((resolve, reject) => {
|
||||
fs.readFile(fullPath, (err, data) => {
|
||||
if (err) {
|
||||
@ -220,7 +226,7 @@ export class DiskMangerWorker {
|
||||
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;
|
||||
|
@ -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 !!(<PhotoDTO>media).metadata.positionData &&
|
||||
!!((<PhotoDTO>media).metadata.positionData.city ||
|
||||
(<PhotoDTO>media).metadata.positionData.state ||
|
||||
(<PhotoDTO>media).metadata.positionData.country ||
|
||||
((<PhotoDTO>media).metadata.positionData.GPSData &&
|
||||
(<PhotoDTO>media).metadata.positionData.GPSData.altitude &&
|
||||
(<PhotoDTO>media).metadata.positionData.GPSData.latitude &&
|
||||
(<PhotoDTO>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 (<PhotoDTO>media).metadata.keywords !== 'undefined' && (<PhotoDTO>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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 ((<PhotoDTO>tmp.media[i]).metadata &&
|
||||
(<PhotoDTO>tmp.media[i]).metadata.positionData &&
|
||||
(<PhotoDTO>tmp.media[i]).metadata.positionData.GPSData &&
|
||||
(<PhotoDTO>tmp.media[i]).metadata.positionData.GPSData.longitude
|
||||
) {
|
||||
this.isPhotoWithLocation = true;
|
||||
break;
|
||||
|
@ -15,11 +15,11 @@ export class GridMedia extends Media {
|
||||
}
|
||||
|
||||
isPhoto(): boolean {
|
||||
return typeof (<PhotoDTO>this.media).metadata.cameraData !== 'undefined';
|
||||
return MediaDTO.isPhoto(this.media);
|
||||
}
|
||||
|
||||
isVideo(): boolean {
|
||||
return typeof (<PhotoDTO>this.media).metadata.cameraData === 'undefined';
|
||||
return MediaDTO.isVideo(this.media);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 (<PhotoDTO>this.gridPhoto.media).metadata.positionData.city ||
|
||||
(<PhotoDTO>this.gridPhoto.media).metadata.positionData.state ||
|
||||
(<PhotoDTO>this.gridPhoto.media).metadata.positionData.country;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 (<PhotoDTO>this.media).metadata.positionData && (<PhotoDTO>this.media).metadata.positionData.GPSData &&
|
||||
(<PhotoDTO>this.media).metadata.positionData.GPSData.latitude && (<PhotoDTO>this.media).metadata.positionData.GPSData.longitude;
|
||||
}
|
||||
|
||||
getPositionText(): string {
|
||||
if (!this.media.metadata.positionData) {
|
||||
if (!(<PhotoDTO>this.media).metadata.positionData) {
|
||||
return '';
|
||||
}
|
||||
let str = this.media.metadata.positionData.city ||
|
||||
this.media.metadata.positionData.state || '';
|
||||
let str = (<PhotoDTO>this.media).metadata.positionData.city ||
|
||||
(<PhotoDTO>this.media).metadata.positionData.state || '';
|
||||
|
||||
if (str.length !== 0) {
|
||||
str += ', ';
|
||||
}
|
||||
str += this.media.metadata.positionData.country || '';
|
||||
str += (<PhotoDTO>this.media).metadata.positionData.country || '';
|
||||
|
||||
return str;
|
||||
}
|
||||
@ -80,7 +81,7 @@ export class InfoPanelLightboxComponent {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.onClose.emit();
|
||||
this.closed.emit();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,11 +5,11 @@
|
||||
</div>
|
||||
|
||||
<div class="lightbox" #lightbox>
|
||||
<app-gallery-lightbox-photo [gridMedia]="activePhoto ? activePhoto.gridPhoto : null"
|
||||
<app-gallery-lightbox-media [gridMedia]="activePhoto ? activePhoto.gridPhoto : null"
|
||||
[loadMedia]="!animating"
|
||||
[windowAspect]="getWindowAspectRatio()"
|
||||
#photo>
|
||||
</app-gallery-lightbox-photo>
|
||||
</app-gallery-lightbox-media>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="controllersVisible"
|
||||
@ -110,7 +110,7 @@
|
||||
id="info-panel"
|
||||
[style.width.px]="infoPanelWidth"
|
||||
[media]="activePhoto.gridPhoto.photo"
|
||||
(onClose)="hideInfoPanel()">
|
||||
(closed)="hideInfoPanel()">
|
||||
|
||||
</app-info-panel>
|
||||
</div>
|
||||
|
@ -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(<any>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) {
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
28
sandbox.ts
28
sandbox.ts
@ -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();
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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((<PhotoDTO>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((<PhotoDTO>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((<PhotoDTO>dir.media[0]).metadata.positionData).to.deep.equals({
|
||||
GPSData: {
|
||||
latitude: 37.871093333333334,
|
||||
longitude: -122.25678,
|
||||
|
Loading…
x
Reference in New Issue
Block a user