Files
photoview/api/src/Scanner.js
2019-07-18 22:33:18 +02:00

265 lines
6.7 KiB
JavaScript

import fs from 'fs-extra'
import path from 'path'
import { resolve as pathResolve, basename as pathBasename } from 'path'
import { PubSub } from 'apollo-server'
import uuid from 'uuid'
import { exiftool } from 'exiftool-vendored'
import sharp from 'sharp'
import readChunk from 'read-chunk'
import imageType from 'image-type'
import config from './config'
export const EVENT_SCANNER_PROGRESS = 'SCANNER_PROGRESS'
const isImage = async path => {
const buffer = await readChunk(path, 0, 12)
const type = imageType(buffer)
return type != null
}
const isRawImage = async path => {
const buffer = await readChunk(path, 0, 12)
const { ext } = imageType(buffer)
const rawTypes = ['cr2', 'arw', 'crw', 'dng']
return rawTypes.includes(ext)
}
class PhotoScanner {
constructor(driver) {
this.driver = driver
this.isRunning = false
this.pubsub = new PubSub()
this.scanAll = this.scanAll.bind(this)
this.scanAlbum = this.scanAlbum.bind(this)
this.scanUser = this.scanUser.bind(this)
this.processImage = this.processImage.bind(this)
}
async scanAll() {
this.isRunning = true
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
scannerStatusUpdate: {
progress: 0,
finished: false,
error: false,
errorMessage: '',
},
})
let session = this.driver.session()
session.run('MATCH (u:User) return u').subscribe({
onNext: record => {
const user = record.toObject().u.properties
console.log('USER', user)
if (!user.rootPath) {
console.log(`User ${user.username}, has no root path, skipping`)
return
}
this.scanUser(user)
console.log(`Scanning ${user.username}...`)
},
onCompleted: () => {
session.close()
this.isRunning = false
console.log('Done scanning')
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
scannerStatusUpdate: {
progress: 100,
finished: true,
error: false,
errorMessage: '',
},
})
},
onError: error => {
console.error(error)
this.pubsub.publish(EVENT_SCANNER_PROGRESS, {
scannerStatusUpdate: {
progress: 0,
finished: false,
error: true,
errorMessage: error.message,
},
})
},
})
}
async scanUser(user) {
console.log('Scanning path', user.rootPath)
const driver = this.driver
const scanAlbum = this.scanAlbum
async function scanPath(path) {
const list = fs.readdirSync(path)
let foundImage = false
for (const item of list) {
const itemPath = pathResolve(path, item)
// console.log(`Scanning item ${itemPath}...`)
const stat = fs.statSync(itemPath)
if (stat.isDirectory()) {
// console.log(`Entering directory ${itemPath}`)
const imagesInDirectory = await scanPath(itemPath)
if (imagesInDirectory) {
console.log(`Found album at ${itemPath}`)
const session = driver.session()
const findAlbumResult = await session.run(
'MATCH (a:Album { path: {path} }) RETURN a',
{
path: itemPath,
}
)
console.log('FIND ALBUM RESULT', findAlbumResult.records)
if (findAlbumResult.records.length != 0) {
console.log('Album already exists')
const album = findAlbumResult.records[0].toObject().a.properties
scanAlbum(album)
continue
}
console.log('Adding album')
const albumId = uuid()
const albumResult = await session.run(
`MATCH (u:User { id: {userId} })
CREATE (a:Album { id: {id}, title: {title}, path: {path} })
CREATE (u)-[own:OWNS]->(a)
RETURN a`,
{
id: albumId,
userId: user.id,
title: item,
path: itemPath,
}
)
const album = albumResult.records[0].toObject().a.properties
scanAlbum(album)
session.close()
}
continue
}
if (!foundImage && (await isImage(itemPath))) {
foundImage = true
}
}
return foundImage
}
await scanPath(user.rootPath)
console.log('User scan complete')
}
async scanAlbum(album) {
const { title, path, id } = album
console.log('Scanning album', title)
const list = fs.readdirSync(path)
for (const item of list) {
const itemPath = pathResolve(path, item)
if (await isImage(itemPath)) {
const session = this.driver.session()
const photoResult = await session.run(
`MATCH (p:Photo {path: {imgPath} })<--(a:Album {id: {albumId}}) RETURN p`,
{
imgPath: itemPath,
albumId: id,
}
)
if (photoResult.records.length != 0) {
console.log(`Photo already exists ${item}`)
} else {
console.log(`Found new image at ${itemPath}`)
const imageId = uuid()
await session.run(
`MATCH (a:Album { id: {albumId} })
CREATE (p:Photo {id: {id}, path: {path}, title: {title} })
CREATE (a)-[:CONTAINS]->(p)`,
{
id: imageId,
path: itemPath,
title: item,
albumId: id,
}
)
this.processImage(imageId)
}
}
}
}
async processImage(id) {
console.log('Processing image')
const session = this.driver.session()
const result = await session.run('MATCH (p:Photo { id: {id} }) return p', {
id,
})
const photo = result.records[0].get('p').properties
console.log('PHOTO', photo.path)
const imagePath = path.resolve(config.cachePath, 'images', id)
await fs.remove(imagePath)
await fs.mkdirp(imagePath)
let resizeBaseImg = photo.path
if (await isRawImage(photo.path)) {
console.log('Processing RAW image')
const extractedPath = path.resolve(imagePath, 'extracted.jpg')
await exiftool.extractPreview(photo.path, extractedPath)
resizeBaseImg = extractedPath
}
// Resize image
console.log('Resizing image', resizeBaseImg)
await sharp(resizeBaseImg)
.resize(1440, 1080, { fit: 'inside', withoutEnlargement: true })
.toFile(path.resolve(imagePath, 'thumbnail.jpg'))
await sharp(resizeBaseImg)
.jpeg({ quality: 85 })
.toFile(path.resolve(imagePath, 'original.jpg'))
session.close()
console.log('Processing done')
}
}
export default PhotoScanner