Add EXIF to sideview

This commit is contained in:
viktorstrate
2019-07-24 20:56:15 +02:00
parent fc87fabab5
commit 53415714c2
12 changed files with 245 additions and 93 deletions

View File

@@ -64,7 +64,7 @@ class PhotoScanner {
await _processImage( await _processImage(
{ {
driver: this.driver, driver: this.driver,
addFinishedImage: this.markFinishedImage, markFinishedImage: this.markFinishedImage,
}, },
id id
) )

View File

@@ -3,8 +3,48 @@ import path from 'path'
import { exiftool } from 'exiftool-vendored' import { exiftool } from 'exiftool-vendored'
import sharp from 'sharp' import sharp from 'sharp'
import { isRawImage, imageSize, getImageCachePath } from './utils' import { isRawImage, imageSize, getImageCachePath } from './utils'
import { DateTime as NeoDateTime } from 'neo4j-driver/lib/v1/temporal-types.js'
export default async function processImage({ driver, addFinishedImage }, id) { async function addExifTags({ session, photo }) {
const exifResult = await session.run(
'MATCH (p:Photo { id: {id} })-[:EXIF]->(exif:PhotoEXIF) RETURN exif',
{
id: photo.id,
}
)
if (exifResult.records.length > 0) return
const rawTags = await exiftool.read(photo.path)
const photoExif = {
camera: rawTags.Model,
maker: rawTags.Make,
lens: rawTags.LensType,
dateShot:
rawTags.DateTimeOriginal &&
NeoDateTime.fromStandardDate(rawTags.DateTimeOriginal.toDate()),
fileSize: rawTags.FileSize,
exposure: rawTags.ShutterSpeedValue,
aperture: rawTags.ApertureValue,
iso: rawTags.ISO,
focalLength: rawTags.FocalLength,
flash: rawTags.Flash,
}
const result = await session.run(
`MATCH (p:Photo { id: {id} })
CREATE (p)-[:EXIF]->(exif:PhotoEXIF {exifProps})`,
{
id: photo.id,
exifProps: photoExif,
}
)
console.log('Added exif tags to photo', photo.path)
}
export default async function processImage({ driver, markFinishedImage }, id) {
const session = driver.session() const session = driver.session()
const result = await session.run( const result = await session.run(
@@ -14,18 +54,32 @@ export default async function processImage({ driver, addFinishedImage }, id) {
} }
) )
const photo = result.records[0].get('p').properties
const albumId = result.records[0].get('a.id')
const imagePath = getImageCachePath(id, albumId)
// Verify that processing is needed
if (await fs.exists(path.resolve(imagePath, 'thumbnail.jpg'))) {
const urlResult = await session.run(
`MATCH (p:Photo { id: {id} })-->(urls:PhotoURL) RETURN urls`,
{ id }
)
if (urlResult.records.length == 2) {
markFinishedImage()
console.log('Skipping image', photo.path)
return
}
}
// Begin processing
await session.run( await session.run(
`MATCH (p:Photo { id: {id} })-->(urls:PhotoURL) DETACH DELETE urls`, `MATCH (p:Photo { id: {id} })-->(urls:PhotoURL) DETACH DELETE urls`,
{ id } { id }
) )
const photo = result.records[0].get('p').properties
const albumId = result.records[0].get('a.id')
// console.log('Processing photo', photo.path)
const imagePath = getImageCachePath(id, albumId)
await fs.remove(imagePath) await fs.remove(imagePath)
await fs.mkdirp(imagePath) await fs.mkdirp(imagePath)
@@ -106,7 +160,9 @@ export default async function processImage({ driver, addFinishedImage }, id) {
console.log('Create photo url failed', e) console.log('Create photo url failed', e)
} }
await addExifTags({ session, photo })
session.close() session.close()
addFinishedImage() markFinishedImage()
} }

View File

@@ -42,11 +42,7 @@ export default async function scanAlbum(
'thumbnail.jpg' 'thumbnail.jpg'
) )
if (!(await fs.exists(thumbnailPath))) { processingImagePromises.push(processImage(photoId))
processingImagePromises.push(processImage(photoId))
} else {
markFinishedImage()
}
} else { } else {
console.log(`Found new image at ${itemPath}`) console.log(`Found new image at ${itemPath}`)
const imageId = uuid() const imageId = uuid()

View File

@@ -134,7 +134,7 @@ export default async function scanUser({ driver, scanAlbum }, user) {
OPTIONAL MATCH (a)-[:CONTAINS]->(p:Photo)-->(photoTail) OPTIONAL MATCH (a)-[:CONTAINS]->(p:Photo)-->(photoTail)
WITH a, p, photoTail, a.id AS albumId WITH a, p, photoTail, a.id AS albumId
DETACH DELETE a, p, photoTail DETACH DELETE a, p, photoTail
RETURN albumId`, RETURN DISTINCT albumId`,
{ userId: user.id, foundAlbums: foundAlbumIds } { userId: user.id, foundAlbums: foundAlbumIds }
) )

View File

@@ -31,6 +31,20 @@ type PhotoURL {
height: Int height: Int
} }
type PhotoEXIF {
photo: Photo @relation(name: "EXIF", direction: "IN")
camera: String
maker: String
lens: String
dateShot: DateTime
fileSize: String
exposure: String
aperture: Float
iso: Int
focalLength: String
flash: String
}
type Photo { type Photo {
id: ID! id: ID!
title: String title: String
@@ -42,6 +56,7 @@ type Photo {
thumbnail: PhotoURL @relation(name: "THUMBNAIL_URL", direction: "OUT") thumbnail: PhotoURL @relation(name: "THUMBNAIL_URL", direction: "OUT")
# The album that holds the photo # The album that holds the photo
album: Album! @relation(name: "CONTAINS", direction: "IN") album: Album! @relation(name: "CONTAINS", direction: "IN")
exif: PhotoEXIF @relation(name: "EXIF", direction: "OUT")
} }
type SiteInfo { type SiteInfo {

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { Query } from 'react-apollo' import { Query } from 'react-apollo'
import Layout from '../../Layout' import Layout from '../../Layout'
import AlbumSidebar from './AlbumSidebar' import PhotoSidebar from '../../components/sidebar/PhotoSidebar'
import PhotoGallery from '../../PhotoGallery' import PhotoGallery from '../../PhotoGallery'
import AlbumGallery from '../AllAlbumsPage/AlbumGallery' import AlbumGallery from '../AllAlbumsPage/AlbumGallery'
@@ -92,7 +92,7 @@ class AlbumPage extends Component {
this.setActiveImage(index, data.album.photos[index].id) this.setActiveImage(index, data.album.photos[index].id)
}} }}
/> />
<AlbumSidebar imageId={this.state.activeImageId} /> <PhotoSidebar imageId={this.state.activeImageId} />
</div> </div>
) )
}} }}

View File

@@ -1,71 +0,0 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
const photoQuery = gql`
query sidebarPhoto($id: ID) {
photo(id: $id) {
title
original {
url
width
height
}
}
}
`
const RightSidebar = styled.div`
height: 100%;
width: 500px;
position: fixed;
right: 0;
top: 60px;
background-color: white;
padding: 12px;
border-left: 1px solid #eee;
`
const PreviewImage = styled.img`
width: 100%;
height: 333px;
object-fit: contain;
`
const Name = styled.div`
text-align: center;
font-size: 16px;
`
class AlbumSidebar extends Component {
render() {
const { imageId } = this.props
if (!imageId) {
return <RightSidebar />
}
return (
<RightSidebar>
<Query query={photoQuery} variables={{ id: imageId }}>
{({ loading, error, data }) => {
if (loading) return 'Loading...'
if (error) return error
const { photo } = data
return (
<div>
<PreviewImage src={photo.original.url} />
<Name>{photo.title}</Name>
</div>
)
}}
</Query>
</RightSidebar>
)
}
}
export default AlbumSidebar

View File

@@ -32,7 +32,13 @@ export const AlbumBox = ({ album, ...props }) => {
return ( return (
<AlbumBoxLink {...props} to={`/album/${album.id}`}> <AlbumBoxLink {...props} to={`/album/${album.id}`}>
<AlbumBoxImage image={album.photos[0] && album.photos[0].thumbnail.url} /> <AlbumBoxImage
image={
album.photos[0] &&
album.photos[0].thumbnail &&
album.photos[0].thumbnail.url
}
/>
<p>{album.title}</p> <p>{album.title}</p>
</AlbumBoxLink> </AlbumBoxLink>
) )

View File

@@ -3,7 +3,7 @@ import Layout from '../../Layout'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { Query } from 'react-apollo' import { Query } from 'react-apollo'
import PhotoGallery from '../../PhotoGallery' import PhotoGallery from '../../PhotoGallery'
import AlbumSidebar from '../AlbumPage/AlbumSidebar' import PhotoSidebar from '../../components/sidebar/PhotoSidebar'
const photoQuery = gql` const photoQuery = gql`
query allPhotosPage { query allPhotosPage {
@@ -79,7 +79,7 @@ class PhotosPage extends Component {
return ( return (
<div> <div>
{galleryGroups} {galleryGroups}
<AlbumSidebar imageId={activeImage} /> <PhotoSidebar imageId={activeImage} />
</div> </div>
) )
}} }}

View File

@@ -131,7 +131,7 @@ class PhotoGallery extends React.Component {
onSelectImage && onSelectImage(index) onSelectImage && onSelectImage(index)
}} }}
> >
<Photo src={photo.thumbnail.url} /> <Photo src={photo.thumbnail && photo.thumbnail.url} />
<PhotoOverlay active={active} /> <PhotoOverlay active={active} />
</PhotoContainer> </PhotoContainer>
) )

View File

@@ -0,0 +1,127 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
import { SidebarItem } from './SidebarItem'
const photoQuery = gql`
query sidebarPhoto($id: ID) {
photo(id: $id) {
title
original {
url
width
height
}
exif {
camera
maker
lens
dateShot {
formatted
}
fileSize
exposure
aperture
iso
focalLength
flash
}
}
}
`
const RightSidebar = styled.div`
height: 100%;
width: 500px;
position: fixed;
right: 0;
top: 60px;
background-color: white;
padding: 12px;
border-left: 1px solid #eee;
`
const PreviewImage = styled.img`
width: 100%;
height: 333px;
object-fit: contain;
`
const Name = styled.div`
text-align: center;
font-size: 16px;
margin-bottom: 12px;
`
const exifNameLookup = {
camera: 'Camera',
maker: 'Maker',
lens: 'Lens',
dateShot: 'Date Shot',
fileSize: 'File Size',
exposure: 'Exposure',
aperture: 'Aperture',
iso: 'ISO',
focalLength: 'Focal Length',
flash: 'Flash',
}
class AlbumSidebar extends Component {
render() {
const { imageId } = this.props
if (!imageId) {
return <RightSidebar />
}
return (
<RightSidebar>
<Query query={photoQuery} variables={{ id: imageId }}>
{({ loading, error, data }) => {
if (loading) return 'Loading...'
if (error) return error
const { photo } = data
let exifItems = []
if (photo.exif) {
let exifKeys = Object.keys(photo.exif).filter(
x => !!photo.exif[x] && x != '__typename'
)
let exif = exifKeys.reduce(
(prev, curr) => ({
...prev,
[curr]: photo.exif[curr],
}),
{}
)
exif.dateShot = exif.dateShot.formatted
exifItems = exifKeys.map(key => (
<SidebarItem
key={key}
name={exifNameLookup[key]}
value={exif[key]}
/>
))
}
return (
<div>
<PreviewImage src={photo.original && photo.original.url} />
<Name>{photo.title}</Name>
<div>{exifItems}</div>
</div>
)
}}
</Query>
</RightSidebar>
)
}
}
export default AlbumSidebar

View File

@@ -0,0 +1,23 @@
import React from 'react'
import styled from 'styled-components'
const ItemName = styled.div`
display: inline-block;
width: 100px;
font-weight: 600;
font-size: 12px;
color: #888;
text-align: right;
margin-right: 8px;
`
const ItemValue = styled.div`
display: inline-block;
`
export const SidebarItem = ({ name, value }) => (
<div>
<ItemName>{name}</ItemName>
<ItemValue>{value}</ItemValue>
</div>
)