diff --git a/api/src/scanner/Scanner.js b/api/src/scanner/Scanner.js index 9d45e911..e5cdeafa 100644 --- a/api/src/scanner/Scanner.js +++ b/api/src/scanner/Scanner.js @@ -64,7 +64,7 @@ class PhotoScanner { await _processImage( { driver: this.driver, - addFinishedImage: this.markFinishedImage, + markFinishedImage: this.markFinishedImage, }, id ) diff --git a/api/src/scanner/processImage.js b/api/src/scanner/processImage.js index 038c4e56..bb588e12 100644 --- a/api/src/scanner/processImage.js +++ b/api/src/scanner/processImage.js @@ -3,8 +3,48 @@ import path from 'path' import { exiftool } from 'exiftool-vendored' import sharp from 'sharp' 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 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( `MATCH (p:Photo { id: {id} })-->(urls:PhotoURL) DETACH DELETE urls`, { 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.mkdirp(imagePath) @@ -106,7 +160,9 @@ export default async function processImage({ driver, addFinishedImage }, id) { console.log('Create photo url failed', e) } + await addExifTags({ session, photo }) + session.close() - addFinishedImage() + markFinishedImage() } diff --git a/api/src/scanner/scanAlbum.js b/api/src/scanner/scanAlbum.js index 17ef1480..e0da59a2 100644 --- a/api/src/scanner/scanAlbum.js +++ b/api/src/scanner/scanAlbum.js @@ -42,11 +42,7 @@ export default async function scanAlbum( 'thumbnail.jpg' ) - if (!(await fs.exists(thumbnailPath))) { - processingImagePromises.push(processImage(photoId)) - } else { - markFinishedImage() - } + processingImagePromises.push(processImage(photoId)) } else { console.log(`Found new image at ${itemPath}`) const imageId = uuid() diff --git a/api/src/scanner/scanUser.js b/api/src/scanner/scanUser.js index 28c9bf84..3152f0b5 100644 --- a/api/src/scanner/scanUser.js +++ b/api/src/scanner/scanUser.js @@ -134,7 +134,7 @@ export default async function scanUser({ driver, scanAlbum }, user) { OPTIONAL MATCH (a)-[:CONTAINS]->(p:Photo)-->(photoTail) WITH a, p, photoTail, a.id AS albumId DETACH DELETE a, p, photoTail - RETURN albumId`, + RETURN DISTINCT albumId`, { userId: user.id, foundAlbums: foundAlbumIds } ) diff --git a/api/src/schema.graphql b/api/src/schema.graphql index a9f2ebe1..4892eb00 100644 --- a/api/src/schema.graphql +++ b/api/src/schema.graphql @@ -31,6 +31,20 @@ type PhotoURL { 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 { id: ID! title: String @@ -42,6 +56,7 @@ type Photo { thumbnail: PhotoURL @relation(name: "THUMBNAIL_URL", direction: "OUT") # The album that holds the photo album: Album! @relation(name: "CONTAINS", direction: "IN") + exif: PhotoEXIF @relation(name: "EXIF", direction: "OUT") } type SiteInfo { diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js index f2540d14..71ae6d1b 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import gql from 'graphql-tag' import { Query } from 'react-apollo' import Layout from '../../Layout' -import AlbumSidebar from './AlbumSidebar' +import PhotoSidebar from '../../components/sidebar/PhotoSidebar' import PhotoGallery from '../../PhotoGallery' import AlbumGallery from '../AllAlbumsPage/AlbumGallery' @@ -92,7 +92,7 @@ class AlbumPage extends Component { this.setActiveImage(index, data.album.photos[index].id) }} /> - + ) }} diff --git a/ui/src/Pages/AlbumPage/AlbumSidebar.js b/ui/src/Pages/AlbumPage/AlbumSidebar.js deleted file mode 100644 index 60a91d5e..00000000 --- a/ui/src/Pages/AlbumPage/AlbumSidebar.js +++ /dev/null @@ -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 - } - - return ( - - - {({ loading, error, data }) => { - if (loading) return 'Loading...' - if (error) return error - - const { photo } = data - - return ( -
- - {photo.title} -
- ) - }} -
-
- ) - } -} - -export default AlbumSidebar diff --git a/ui/src/Pages/AllAlbumsPage/AlbumBox.js b/ui/src/Pages/AllAlbumsPage/AlbumBox.js index 1bdf180d..8f6f2ecb 100644 --- a/ui/src/Pages/AllAlbumsPage/AlbumBox.js +++ b/ui/src/Pages/AllAlbumsPage/AlbumBox.js @@ -32,7 +32,13 @@ export const AlbumBox = ({ album, ...props }) => { return ( - +

{album.title}

) diff --git a/ui/src/Pages/PhotosPage/PhotosPage.js b/ui/src/Pages/PhotosPage/PhotosPage.js index 1277dbe1..2363b765 100644 --- a/ui/src/Pages/PhotosPage/PhotosPage.js +++ b/ui/src/Pages/PhotosPage/PhotosPage.js @@ -3,7 +3,7 @@ import Layout from '../../Layout' import gql from 'graphql-tag' import { Query } from 'react-apollo' import PhotoGallery from '../../PhotoGallery' -import AlbumSidebar from '../AlbumPage/AlbumSidebar' +import PhotoSidebar from '../../components/sidebar/PhotoSidebar' const photoQuery = gql` query allPhotosPage { @@ -79,7 +79,7 @@ class PhotosPage extends Component { return (
{galleryGroups} - +
) }} diff --git a/ui/src/PhotoGallery.js b/ui/src/PhotoGallery.js index bad85ab2..9fa96670 100644 --- a/ui/src/PhotoGallery.js +++ b/ui/src/PhotoGallery.js @@ -131,7 +131,7 @@ class PhotoGallery extends React.Component { onSelectImage && onSelectImage(index) }} > - + ) diff --git a/ui/src/components/sidebar/PhotoSidebar.js b/ui/src/components/sidebar/PhotoSidebar.js new file mode 100644 index 00000000..36e3eb7a --- /dev/null +++ b/ui/src/components/sidebar/PhotoSidebar.js @@ -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 + } + + return ( + + + {({ 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 => ( + + )) + } + + return ( +
+ + {photo.title} +
{exifItems}
+
+ ) + }} +
+
+ ) + } +} + +export default AlbumSidebar diff --git a/ui/src/components/sidebar/SidebarItem.js b/ui/src/components/sidebar/SidebarItem.js new file mode 100644 index 00000000..7849301c --- /dev/null +++ b/ui/src/components/sidebar/SidebarItem.js @@ -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 }) => ( +
+ {name} + {value} +
+)