mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:09:05 +00:00
Add EXIF to sideview
This commit is contained in:
@@ -64,7 +64,7 @@ class PhotoScanner {
|
|||||||
await _processImage(
|
await _processImage(
|
||||||
{
|
{
|
||||||
driver: this.driver,
|
driver: this.driver,
|
||||||
addFinishedImage: this.markFinishedImage,
|
markFinishedImage: this.markFinishedImage,
|
||||||
},
|
},
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
127
ui/src/components/sidebar/PhotoSidebar.js
Normal file
127
ui/src/components/sidebar/PhotoSidebar.js
Normal 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
|
||||||
23
ui/src/components/sidebar/SidebarItem.js
Normal file
23
ui/src/components/sidebar/SidebarItem.js
Normal 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>
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user