Allow for multiple root directories

This necessitates a slight modification to the config file
($HOME/forkalsrud.org/photo.properties).  Instead of just "base" and "dbdir",  Each root
has to have "root." + name + ".base"
and "root." + name + ".dbdir"
This commit is contained in:
Knut Forkalsrud 2023-11-04 13:37:03 -07:00
parent cb2a20ed2c
commit 9fac9425ce
14 changed files with 703 additions and 586 deletions

View file

@ -38,8 +38,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>

View file

@ -36,6 +36,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
boolean childrenLoaded = false;
Comparator<Entry> sort = null;
Date earliest = null;
boolean groupByYear;
public DirectoryEntry(ServiceApi api, File file) {
super(file);
@ -197,6 +198,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
}
}
this.earliest = oldest;
this.groupByYear = "year".equalsIgnoreCase(props.getProperty("group"));
if (thumbnail == null && !children.isEmpty()) {
setThumbnail(children.get(0).getThumbnail());
}
@ -212,7 +214,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
@Override
public boolean groupByYear() {
return parent == null;
return this.groupByYear;
}

View file

@ -1,18 +1,20 @@
package org.forkalsrud.album.exif;
import java.io.File;
import java.io.IOException;
import org.forkalsrud.album.db.DirectoryDatabase;
import org.forkalsrud.album.video.MovieCoder;
import org.forkalsrud.album.video.MovieMetadataGenerator;
public class DirectoryEntryFactory implements DirectoryEntry.ServiceApi {
private DirectoryDatabase dirDb;
private DirectoryMetadataGenerator generator;
private MovieMetadataGenerator movieGenerator;
public DirectoryEntryFactory(DirectoryDatabase dirDb, MovieCoder movieCoder) {
public DirectoryEntryFactory(DirectoryDatabase dirDb) throws IOException, InterruptedException {
this.dirDb = dirDb;
this.generator = new DirectoryMetadataGenerator(movieCoder);
this.generator = new DirectoryMetadataGenerator(new MovieMetadataGenerator());
}

View file

@ -19,13 +19,11 @@ import javax.imageio.stream.ImageInputStream;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.imaging.jpeg.JpegProcessingException;
import org.forkalsrud.album.db.DirectoryProps;
import org.forkalsrud.album.video.MovieCoder;
import org.forkalsrud.album.video.MovieMetadataGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
@ -39,10 +37,10 @@ public class DirectoryMetadataGenerator {
final static String CACHE_FILE = "cache.properties";
final static String OVERRIDE_FILE = "album.properties";
MovieCoder movieCoder;
MovieMetadataGenerator movieMetadataGenerator;
public DirectoryMetadataGenerator(MovieCoder movieCoder) {
this.movieCoder = movieCoder;
public DirectoryMetadataGenerator(MovieMetadataGenerator generator) {
this.movieMetadataGenerator = generator;
}
@ -91,7 +89,7 @@ public class DirectoryMetadataGenerator {
addPropsForFile(props, f, p);
}
} else if (name.endsWith(".mov") || name.endsWith(".MOV") || name.endsWith(".mp4") || name.endsWith(".m4v") || name.endsWith(".avi")) {
Map<String, String> p = movieCoder.generateVideoProperties(f);
Map<String, String> p = movieMetadataGenerator.generateVideoProperties(f);
addPropsForFile(props, f, p);
}
}

View file

@ -1,7 +1,6 @@
package org.forkalsrud.album.exif;
import java.io.File;
import java.nio.file.Path;
import java.util.LinkedList;
public class SearchEngine {

View file

@ -21,6 +21,9 @@ public class SearchResults extends EntryWithChildren<SearchEntry> {
protected SearchResults(DirectoryEntry root) {
super(root);
this.next = root.next;
this.prev = root.prev;
this.parent = root.parent;
}
public void addMatch(Entry entry) {

View file

@ -6,15 +6,10 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.forkalsrud.album.db.Chunk;
import org.forkalsrud.album.db.MovieDatabase;
import org.forkalsrud.album.exif.Dimension;
@ -28,14 +23,11 @@ public class MovieCoder {
private String ffmpegExecutable;
private String mplayerExecutable;
private String exiftoolExecutable;
private PictureScaler pictureScaler;
private MovieDatabase movieDb;
private HashMap<String, EncodingProcess> currentEncodings = new HashMap<String, EncodingProcess>();
public MovieCoder(PictureScaler pictureScaler, MovieDatabase movieDb) {
public MovieCoder(PictureScaler pictureScaler) {
this.pictureScaler = pictureScaler;
this.movieDb = movieDb;
}
public void init() throws Exception {
@ -45,175 +37,6 @@ public class MovieCoder {
this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg");
}
this.mplayerExecutable = util.findExecutableInShellPath("mplayer");
this.exiftoolExecutable = util.findExecutableInShellPath("exiftool");
}
/**
*
* @param f the movie file
* @return a map with the following keys: orientation dimensions captureDate comment etag
* @throws IOException
* @throws InterruptedException
*/
public Map<String, String> generateVideoProperties(File f) throws IOException, InterruptedException {
/*
* [{
"SourceFile": "/home/erik/local/IMG_0837.mov",
"ExifToolVersion": 9.12,
"FileName": "IMG_0837.mov",
"Directory": "/home/erik/local",
"FileSize": "1438 kB",
"FileModifyDate": "2013:01:19 18:36:53-08:00",
"FileAccessDate": "2013:01:19 18:36:57-08:00",
"FileInodeChangeDate": "2013:01:19 18:36:53-08:00",
"FilePermissions": "rw-rw-r--",
"FileType": "MOV",
"MIMEType": "video/quicktime",
"MajorBrand": "Apple QuickTime (.MOV/QT)",
"MinorVersion": "0.0.0",
"CompatibleBrands": ["qt "],
"MovieDataSize": 1467141,
"MovieHeaderVersion": 0,
"ModifyDate": "2012:12:30 06:44:31",
"TimeScale": 600,
"Duration": "9.07 s",
"PreferredRate": 1,
"PreferredVolume": "100.00%",
"PreviewTime": "0 s",
"PreviewDuration": "0 s",
"PosterTime": "0 s",
"SelectionTime": "0 s",
"SelectionDuration": "0 s",
"CurrentTime": "0 s",
"NextTrackID": 3,
"TrackHeaderVersion": 0,
"TrackCreateDate": "2012:12:30 06:44:26",
"TrackModifyDate": "2012:12:30 06:44:31",
"TrackID": 1,
"TrackDuration": "9.05 s",
"TrackLayer": 0,
"TrackVolume": "100.00%",
"Balance": 0,
"AudioChannels": 1,
"AudioBitsPerSample": 16,
"AudioSampleRate": 44100,
"AudioFormat": "chan",
"MatrixStructure": "0 1 0 -1 0 0 540 0 1",
"ImageWidth": 960,
"ImageHeight": 540,
"CleanApertureDimensions": "960x540",
"ProductionApertureDimensions": "960x540",
"EncodedPixelsDimensions": "960x540",
"MediaHeaderVersion": 0,
"MediaCreateDate": "2012:12:30 06:44:26",
"MediaModifyDate": "2012:12:30 06:44:31",
"MediaTimeScale": 600,
"MediaDuration": "9.10 s",
"MediaLanguageCode": "und",
"GraphicsMode": "ditherCopy",
"OpColor": "32768 32768 32768",
"HandlerClass": "Data Handler",
"HandlerVendorID": "Apple",
"HandlerDescription": "Core Media Data Handler",
"CompressorID": "avc1",
"SourceImageWidth": 960,
"SourceImageHeight": 540,
"XResolution": 72,
"YResolution": 72,
"CompressorName": "H.264",
"BitDepth": 24,
"VideoFrameRate": 30,
"CameraIdentifier": "Back",
"FrameReadoutTime": "28512 microseconds",
"Make": "Apple",
"SoftwareVersion": "6.0.1",
"CreateDate": "2012:12:29 16:30:21-08:00",
"Model": "iPhone 4S",
"HandlerType": "Metadata Tags",
"Make-und-US": "Apple",
"CreationDate-und-US": "2012:12:29 16:30:21-08:00",
"Software-und-US": "6.0.1",
"Model-und-US": "iPhone 4S",
"AvgBitrate": "1.29 Mbps",
"ImageSize": "960x540",
"Rotation": 90
}]
*/
Map<String, String> props = new HashMap<String, String>();
ProcessBuilder pb = new ProcessBuilder().command(
this.exiftoolExecutable, "-j", f.getAbsolutePath());
pb.redirectErrorStream(false);
Process p = pb.start();
p.getOutputStream().close();
ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally
@SuppressWarnings("unchecked")
List<Map<String,Object>> userDataList = mapper.readValue(p.getInputStream(), List.class);
p.waitFor();
props.put("type", "movie");
Map<String, Object> userData = userDataList.get(0);
System.out.println(userData);
{
// The orientation is about flipping and rotating. Here is what an 'F' looks like
// on pictures with each orientation.
//
// 1 2 3 4 5 6 7 8
//
// 888888 888888 88 88 8888888888 88 88 8888888888
// 88 88 88 88 88 88 88 88 88 88 88 88
// 8888 8888 8888 8888 88 8888888888 8888888888 88
// 88 88 88 88
// 88 88 888888 888888
//
// The first four are obtained with only flipping X and/or Y
// The last four are obtained by rotating 90 degrees and then flipping X and/or Y.
//
String orientation;
Object rotationObj = userData.get("Rotation");
if (rotationObj == null) {
orientation = "1";
} else {
String rotation = rotationObj.toString();
if ("0".equals(rotation)) {
orientation = "1";
} else if ("90".equals(rotation)) {
orientation = "6";
} else if ("180".equals(rotation)) {
orientation = "3";
} else if ("270".equals(rotation)) {
orientation = "8";
} else {
log.warn("unknown rotation: " + rotation + " for file " + f);
orientation = "1";
}
}
props.put("orientation", orientation);
}
Object imageSize = userData.get("ImageSize");
if (imageSize == null) {
imageSize = props.get("ImageWidth") + "x" + props.get("ImageHeight");
}
props.put("dimensions", imageSize.toString());
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
props.put("captureDate", sdf.format(new Date(f.lastModified())));
props.put("etag", Integer.toHexString(f.getName().hashCode() + Long.valueOf(f.lastModified()).hashCode()));
for (String prop : new String[] { "Duration", "MediaDuration", "PlayDuration" }) {
Object o = userData.get(prop);
if (o != null) {
props.put("length", o.toString().split(" ")[0]);
break;
}
}
return props;
}
@ -268,9 +91,9 @@ public class MovieCoder {
* @return
*/
private synchronized EncodingProcess submitEncodingJob(File file,
Thumbnail thumbnail, Dimension targetSize, String key) {
Thumbnail thumbnail, Dimension targetSize, String key, MovieDatabase movieDb) {
EncodingProcess ep;
ep = new EncodingProcess(file, thumbnail, targetSize);
ep = new EncodingProcess(file, thumbnail, targetSize, movieDb);
currentEncodings.put(key, ep);
new Thread(ep).start();
return ep;
@ -295,8 +118,10 @@ public class MovieCoder {
private long fileTimestamp;
private int orientation;
private volatile boolean done = false;
private MovieDatabase movieDb;
public EncodingProcess(File file, Thumbnail thumbnail, Dimension size) {
public EncodingProcess(File file, Thumbnail thumbnail, Dimension size, MovieDatabase movieDb) {
this.file = file;
this.fileTimestamp = file.lastModified();
this.targetSize = size.even();
@ -305,6 +130,7 @@ public class MovieCoder {
extraMeta.setDuration(thumbnail.getDuration());
this.filter = new FlvFilter(this, extraMeta);
this.orientation = thumbnail.getOrientation();
this.movieDb = movieDb;
}
/*
@ -527,10 +353,10 @@ public class MovieCoder {
// TODO (knut 05 JUL 2011) Come up with a better interface for supporting range requests etcetera
public void stream(File file, Thumbnail thumbnail, String size, OutputStream out) {
public void stream(File file, Thumbnail thumbnail, String size, OutputStream out, MovieDatabase movieDb) {
try {
grabStream(file, thumbnail, size).stream(out);
grabStream(file, thumbnail, size, movieDb).stream(out);
} catch (Exception e) {
log.error("stream fail", e);
}
@ -543,7 +369,7 @@ public class MovieCoder {
synchronized VideoStreamer grabStream(File file, Thumbnail thumbnail, String size) {
synchronized VideoStreamer grabStream(File file, Thumbnail thumbnail, String size, MovieDatabase movieDb) {
Dimension targetSize = thumbnail.getSize().scale(size).even();
String key = key(file, targetSize);
@ -563,9 +389,9 @@ public class MovieCoder {
}
// If neither we need to start the encoding process
if (chunk == null && ep == null) {
ep = submitEncodingJob(file, thumbnail, targetSize, key);
ep = submitEncodingJob(file, thumbnail, targetSize, key, movieDb);
}
return new VideoStreamer(key, ep, chunk);
return new VideoStreamer(key, ep, chunk, movieDb);
}
@ -576,11 +402,14 @@ public class MovieCoder {
private Chunk chunk;
private String key;
private boolean done = false;
private MovieDatabase movieDb;
private VideoStreamer(String key, EncodingProcess ep, Chunk chunk0) {
private VideoStreamer(String key, EncodingProcess ep, Chunk chunk0, MovieDatabase movieDb) {
this.key = key;
this.ep = ep;
this.chunk = chunk0;
this.movieDb = movieDb;
// Range requests can hook in here
// if we have chunk metadata in chunk0 we can use that to compute the first
// chunk we want and set this.chunkNo accordingly. Otherwise (not likely

View file

@ -0,0 +1,195 @@
package org.forkalsrud.album.video;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MovieMetadataGenerator {
private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MovieMetadataGenerator.class);
private String exiftoolExecutable;
public MovieMetadataGenerator() throws IOException, InterruptedException {
ExecUtil util = new ExecUtil();
this.exiftoolExecutable = util.findExecutableInShellPath("exiftool");
}
/**
*
* @param f the movie file
* @return a map with the following keys: orientation dimensions captureDate comment etag
* @throws IOException
* @throws InterruptedException
*/
public Map<String, String> generateVideoProperties(File f) throws IOException, InterruptedException {
/*
* [{
"SourceFile": "/home/erik/local/IMG_0837.mov",
"ExifToolVersion": 9.12,
"FileName": "IMG_0837.mov",
"Directory": "/home/erik/local",
"FileSize": "1438 kB",
"FileModifyDate": "2013:01:19 18:36:53-08:00",
"FileAccessDate": "2013:01:19 18:36:57-08:00",
"FileInodeChangeDate": "2013:01:19 18:36:53-08:00",
"FilePermissions": "rw-rw-r--",
"FileType": "MOV",
"MIMEType": "video/quicktime",
"MajorBrand": "Apple QuickTime (.MOV/QT)",
"MinorVersion": "0.0.0",
"CompatibleBrands": ["qt "],
"MovieDataSize": 1467141,
"MovieHeaderVersion": 0,
"ModifyDate": "2012:12:30 06:44:31",
"TimeScale": 600,
"Duration": "9.07 s",
"PreferredRate": 1,
"PreferredVolume": "100.00%",
"PreviewTime": "0 s",
"PreviewDuration": "0 s",
"PosterTime": "0 s",
"SelectionTime": "0 s",
"SelectionDuration": "0 s",
"CurrentTime": "0 s",
"NextTrackID": 3,
"TrackHeaderVersion": 0,
"TrackCreateDate": "2012:12:30 06:44:26",
"TrackModifyDate": "2012:12:30 06:44:31",
"TrackID": 1,
"TrackDuration": "9.05 s",
"TrackLayer": 0,
"TrackVolume": "100.00%",
"Balance": 0,
"AudioChannels": 1,
"AudioBitsPerSample": 16,
"AudioSampleRate": 44100,
"AudioFormat": "chan",
"MatrixStructure": "0 1 0 -1 0 0 540 0 1",
"ImageWidth": 960,
"ImageHeight": 540,
"CleanApertureDimensions": "960x540",
"ProductionApertureDimensions": "960x540",
"EncodedPixelsDimensions": "960x540",
"MediaHeaderVersion": 0,
"MediaCreateDate": "2012:12:30 06:44:26",
"MediaModifyDate": "2012:12:30 06:44:31",
"MediaTimeScale": 600,
"MediaDuration": "9.10 s",
"MediaLanguageCode": "und",
"GraphicsMode": "ditherCopy",
"OpColor": "32768 32768 32768",
"HandlerClass": "Data Handler",
"HandlerVendorID": "Apple",
"HandlerDescription": "Core Media Data Handler",
"CompressorID": "avc1",
"SourceImageWidth": 960,
"SourceImageHeight": 540,
"XResolution": 72,
"YResolution": 72,
"CompressorName": "H.264",
"BitDepth": 24,
"VideoFrameRate": 30,
"CameraIdentifier": "Back",
"FrameReadoutTime": "28512 microseconds",
"Make": "Apple",
"SoftwareVersion": "6.0.1",
"CreateDate": "2012:12:29 16:30:21-08:00",
"Model": "iPhone 4S",
"HandlerType": "Metadata Tags",
"Make-und-US": "Apple",
"CreationDate-und-US": "2012:12:29 16:30:21-08:00",
"Software-und-US": "6.0.1",
"Model-und-US": "iPhone 4S",
"AvgBitrate": "1.29 Mbps",
"ImageSize": "960x540",
"Rotation": 90
}]
*/
Map<String, String> props = new HashMap<String, String>();
ProcessBuilder pb = new ProcessBuilder().command(
this.exiftoolExecutable, "-j", f.getAbsolutePath());
pb.redirectErrorStream(false);
Process p = pb.start();
p.getOutputStream().close();
ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally
@SuppressWarnings("unchecked")
List<Map<String,Object>> userDataList = mapper.readValue(p.getInputStream(), List.class);
p.waitFor();
props.put("type", "movie");
Map<String, Object> userData = userDataList.get(0);
System.out.println(userData);
{
// The orientation is about flipping and rotating. Here is what an 'F' looks like
// on pictures with each orientation.
//
// 1 2 3 4 5 6 7 8
//
// 888888 888888 88 88 8888888888 88 88 8888888888
// 88 88 88 88 88 88 88 88 88 88 88 88
// 8888 8888 8888 8888 88 8888888888 8888888888 88
// 88 88 88 88
// 88 88 888888 888888
//
// The first four are obtained with only flipping X and/or Y
// The last four are obtained by rotating 90 degrees and then flipping X and/or Y.
//
String orientation;
Object rotationObj = userData.get("Rotation");
if (rotationObj == null) {
orientation = "1";
} else {
String rotation = rotationObj.toString();
if ("0".equals(rotation)) {
orientation = "1";
} else if ("90".equals(rotation)) {
orientation = "6";
} else if ("180".equals(rotation)) {
orientation = "3";
} else if ("270".equals(rotation)) {
orientation = "8";
} else {
log.warn("unknown rotation: " + rotation + " for file " + f);
orientation = "1";
}
}
props.put("orientation", orientation);
}
Object imageSize = userData.get("ImageSize");
if (imageSize == null) {
imageSize = props.get("ImageWidth") + "x" + props.get("ImageHeight");
}
props.put("dimensions", imageSize.toString());
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
props.put("captureDate", sdf.format(new Date(f.lastModified())));
props.put("etag", Integer.toHexString(f.getName().hashCode() + Long.valueOf(f.lastModified()).hashCode()));
for (String prop : new String[] { "Duration", "MediaDuration", "PlayDuration" }) {
Object o = userData.get(prop);
if (o != null) {
props.put("length", o.toString().split(" ")[0]);
break;
}
}
return props;
}
}

View file

@ -7,13 +7,14 @@ import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;
import java.util.*;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
@ -21,6 +22,8 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.apache.log4j.PropertyConfigurator;
import org.forkalsrud.album.db.DirectoryDatabase;
import org.forkalsrud.album.db.DirectoryProps;
@ -39,6 +42,7 @@ import org.springframework.web.util.HtmlUtils;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
public class AlbumServlet
extends HttpServlet
{
@ -94,219 +98,142 @@ public class AlbumServlet
"com.sleepycat.je.recovery.RecoveryManager");
}
/**
* maps files to URIs relative to servlet, e.g. /home/joe/photos/holiday/arrival.jpg -> /photos/holiday/arrival.jpg
* assuming base is /home/joe/photos
*/
public class Mapper {
private File base;
public Mapper(File base) {
this.base = base;
}
public String map(File file) {
StringBuilder buf = new StringBuilder();
return appendFile(buf, file).toString();
}
StringBuilder appendFile(StringBuilder buf, File file) {
if (file == null) {
return buf;
}
if (base.equals(file.getAbsoluteFile())) {
return buf.append("/").append(base.getName());
} else {
return appendFile(buf, file.getParentFile()).append('/').append(file.getName());
}
}
Calendar cal = Calendar.getInstance();
public String year(Date d) {
if (d == null) {
return "";
}
cal.setTime(d);
return String.valueOf(cal.get(Calendar.YEAR));
}
}
class Root {
String name;
File base;
String basePrefix;
PictureScaler pictureScaler;
long lastCacheFlushTime;
DirectoryEntryFactory dirEntryFactory;
private Environment environment;
ThumbnailDatabase thumbDb;
DirectoryDatabase dirDb;
MovieDatabase movieDb;
Entry cachedRootNode = null;
MovieCoder movieCoder;
DirectoryEntryFactory dirEntryFactory;
long nextCacheRefresh;
@Override
public void init()
throws ServletException
{
Properties props = new Properties();
public Root(String name, File base, File dbDir) {
this.name = name;
this.base = base;
this.basePrefix = "/" + base.getName();
File forkalsrudOrg = new File(System.getProperty("user.home"), "forkalsrud.org");
if (! forkalsrudOrg.exists()) {
forkalsrudOrg.mkdirs();
}
File photoConf = new File(forkalsrudOrg, "photo.properties");
if (photoConf.exists()) {
try {
props.load(new FileReader(photoConf));
} catch (Exception e) {
throw new ServletException("unable to load settings from " + photoConf.getPath(), e);
}
}
log4jInit("/log4j.properties");
log.info("in init of Album");
long minute = 60 * 1000L;
nextCacheRefresh = System.currentTimeMillis() + minute;
base = new File(props.getProperty("base", "photos")).getAbsoluteFile();
basePrefix = "/" + base.getName();
String dbDirName = props.getProperty("dbdir");
File dbDir;
if (dbDirName != null) {
dbDir = new File(dbDirName);
} else {
dbDir = new File(System.getProperty("java.io.tmpdir"), "album");
if (dbDir.isDirectory() && dbDir.canWrite()) {
for (File f : dbDir.listFiles()) {
f.delete();
}
}
}
dbDir.mkdirs();
EnvironmentConfig environmentConfig = new EnvironmentConfig();
environmentConfig.setAllowCreate(true);
environmentConfig.setTransactional(true);
environment = new Environment(dbDir, environmentConfig);
this.environment = new Environment(dbDir, environmentConfig);
thumbDb = new ThumbnailDatabase(environment);
dirDb = new DirectoryDatabase(environment);
movieDb = new MovieDatabase(environment);
this.thumbDb = new ThumbnailDatabase(environment);
this.dirDb = new DirectoryDatabase(environment);
this.movieDb = new MovieDatabase(environment);
pictureScaler = new PictureScaler();
movieCoder = new MovieCoder(pictureScaler, movieDb);
movieCoder.setFfmpegPath(props.getProperty("ffmpeg.path"));
try {
movieCoder.init();
} catch (Exception e) {
throw new ServletException("unable to locate movie helpers (mplayer and ffmpeg)", e);
}
dirEntryFactory = new DirectoryEntryFactory(dirDb, movieCoder);
lastCacheFlushTime = System.currentTimeMillis();
}
private void log4jInit(String resource) {
try {
Properties props = new Properties();
props.load(getClass().getResourceAsStream(resource));
PropertyConfigurator.configure(props);
} catch (IOException e) {
throw new RuntimeException(e);
dirEntryFactory = new DirectoryEntryFactory(dirDb);
} catch (IOException | InterruptedException e) {
log.error("initialization of " + name, e);
}
}
@Override
public void flushCache() {
cachedRootNode = null;
}
public void destroy() {
log.info("Shutting down Album");
dirDb.destroy();
thumbDb.destroy();
environment.close();
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
doGet(req, res);
public String getName() {
return name;
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
req.setAttribute("assets", req.getContextPath() + "/assets");
req.setAttribute("req", req);
req.setAttribute("base", req.getContextPath() + req.getServletPath());
req.setAttribute("mapper", new Mapper());
String pathInfo = req.getPathInfo();
// help the user get to the top level page
if (pathInfo == null || "/".equals(pathInfo)) {
String u = req.getContextPath() + "/album/" + base.getName() + ".album";
res.sendRedirect(u);
return;
Entry resolveEntry(String pathInfo) {
if (pathInfo == null || "/".equals(pathInfo)) return resolve(base);
return resolve(new File(base.getParentFile(), pathInfo));
}
if ("/_roots.json".equals(pathInfo)) {
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
PrintWriter out = res.getWriter();
out.append("[\"").append(base.getName()).append("\"]");
return;
}
Entry resolve(File file) {
long now = System.currentTimeMillis();
if (now > nextCacheRefresh) {
cachedRootNode = null;
long minute = 60 * 1000L;
nextCacheRefresh = now + minute;
if (base.equals(file.getAbsoluteFile())) {
synchronized (this) {
if (cachedRootNode == null) {
cachedRootNode = dirEntryFactory.getEntry(file, null);
}
try {
if (pathInfo.startsWith(basePrefix)) {
pathInfo = pathInfo.substring(basePrefix.length());
} else if (pathInfo.equals("/search")) {
handleSearch(req, res, (DirectoryEntry)resolveEntry("/"));
return;
}
return cachedRootNode;
} else {
res.sendError(HttpServletResponse.SC_NOT_FOUND, "pathinfo=" + pathInfo);
return;
return ((DirectoryEntry)resolve(file.getParentFile())).get(file);
}
}
if (pathInfo.endsWith(".album")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".album".length());
handleAlbum(req, res, (DirectoryEntry)resolveEntry(pathInfo));
return;
void handleSearch(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception {
String query = req.getParameter("q");
SearchEngine search = new SearchEngine(entry);
SearchResults results = search.search(query);
res.setContentType("text/html");
req.setAttribute("search", query);
req.setAttribute("entry", results);
req.setAttribute("thmb", new Integer(250));
req.setAttribute("full", new Integer(800));
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
rd.forward(req, res);
}
if (pathInfo.endsWith(".photo")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length());
handlePhoto(req, res, (FileEntry)resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".frame")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".frame".length());
handleMovieFrame(req, res, (FileEntry)resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".movie")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".movie".length());
handleMovie(req, res, (FileEntry)resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".edit")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".edit".length());
handleEdit(req, res, (FileEntry)resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".json")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".json".length());
DirectoryEntry directoryEntry = (DirectoryEntry)resolveEntry(pathInfo);
if (directoryEntry == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
handleJson(req, res, directoryEntry);
return;
}
if (pathInfo.endsWith(".cache")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".cache".length());
handleCache(req, res, (DirectoryEntry)resolveEntry(pathInfo));
return;
}
File file = new File(base, pathInfo);
if (!file.canRead()) {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
String size = req.getParameter("size");
if (size != null) {
FileEntry e = (FileEntry)resolve(file);
procesScaledImageRequest(req, res, file, e.getThumbnail(), size);
return;
}
} catch (Exception e) {
throw new ServletException("sadness", e);
}
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
void handlePhoto(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
res.setContentType("text/html");
@ -343,7 +270,8 @@ public class AlbumServlet
String key = file.getPath() + ":" + secondNo + ":" + size;
CachedImage cimg = thumbDb.load(key);
if (cimg != null) {
if (cimg.lastModified == file.lastModified()) {
long fileTs = file.lastModified();
if (cimg.lastModified >= fileTs) {
log.info("cache hit on " + key);
} else {
log.info(" " + key + " has changed so cache entry wil be refreshed");
@ -388,7 +316,7 @@ public class AlbumServlet
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
// res.setHeader("Cache-control", "no-cache");
res.setContentType("video/x-flv");
movieCoder.stream(entry.getPath(), entry.getThumbnail(), size, res.getOutputStream());
movieCoder.stream(entry.getPath(), entry.getThumbnail(), size, res.getOutputStream(), movieDb);
} catch (Exception ex) {
log.error("darn", ex);
throw new RuntimeException("sadness", ex);
@ -398,7 +326,7 @@ public class AlbumServlet
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
try {
Mapper mapper = new Mapper();
Mapper mapper = new Mapper(base);
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
PrintWriter out = res.getWriter();
@ -508,48 +436,9 @@ public class AlbumServlet
}
void handleSearch(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception {
String query = req.getParameter("q");
SearchEngine search = new SearchEngine(entry);
SearchResults results = search.search(query);
res.setContentType("text/html");
req.setAttribute("search", query);
req.setAttribute("entry", results);
req.setAttribute("thmb", new Integer(250));
req.setAttribute("full", new Integer(800));
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
rd.forward(req, res);
}
boolean etagMatches(HttpServletRequest req, String fileEtag) {
String cacheControl = req.getHeader("Cache-Control");
if ("no-cache".equals(cacheControl)) {
return false;
}
String reqEtag = req.getHeader("If-None-Match");
if (reqEtag != null) {
return reqEtag.equals(fileEtag);
}
return false;
}
boolean notModified(HttpServletRequest req, File f) {
String cacheControl = req.getHeader("Cache-Control");
if ("no-cache".equals(cacheControl)) {
return false;
}
long reqDate = req.getDateHeader("If-Modified-Since");
long fDate = f.lastModified();
return reqDate > 0 && fDate > 0 && fDate <= reqDate;
}
void procesScaledImageRequest(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException {
if (notModified(req, file)) {
@ -590,30 +479,261 @@ public class AlbumServlet
res.setContentLength(cimg.bits.length);
res.getOutputStream().write(cimg.bits);
}
Entry resolveEntry(String pathInfo) {
if (pathInfo == null || "/".equals(pathInfo)) return resolve(base);
return resolve(new File(base, pathInfo));
}
Entry resolve(File file) {
if (base.equals(file.getAbsoluteFile())) {
synchronized (this) {
if (cachedRootNode == null) {
cachedRootNode = dirEntryFactory.getEntry(file, null);
Map<String, Root> roots = new HashMap<String, Root>();
PictureScaler pictureScaler;
MovieCoder movieCoder;
long lastCacheFlushTime;
long nextCacheRefresh;
@Override
public void init()
throws ServletException
{
Properties props = new Properties();
File forkalsrudOrg = new File(System.getProperty("user.home"), "forkalsrud.org");
if (! forkalsrudOrg.exists()) {
forkalsrudOrg.mkdirs();
}
File photoConf = new File(forkalsrudOrg, "photo.properties");
if (photoConf.exists()) {
try {
props.load(new FileReader(photoConf));
} catch (Exception e) {
throw new ServletException("unable to load settings from " + photoConf.getPath(), e);
}
}
return cachedRootNode;
log4jInit("/log4j.properties");
log.info("in init of Album");
long minute = 60 * 1000L;
nextCacheRefresh = System.currentTimeMillis() + minute;
Set<String> rootNames = props.stringPropertyNames().stream()
.filter(s -> s.startsWith("root."))
.map(s -> s.split("\\.")[1])
.collect(Collectors.toSet());
if (rootNames.isEmpty()) {
File photos = new File("photos");
File dbdir = new File(System.getProperty("java.io.tmpdir"), "album");
photos.mkdirs();
dbdir.mkdirs();
roots.put("photos", new Root("photos", photos, dbdir));
} else {
return ((DirectoryEntry)resolve(file.getParentFile())).get(file);
rootNames.forEach(name -> {
File base = new File(props.getProperty("root." + name + ".base", "photos")).getAbsoluteFile();
String dbDirName = props.getProperty("root." + name + ".dbdir");
File dbDir = new File(dbDirName);
dbDir.mkdirs();
roots.put(name, new Root(name, base, dbDir));
});
}
pictureScaler = new PictureScaler();
movieCoder = new MovieCoder(pictureScaler);
movieCoder.setFfmpegPath(props.getProperty("ffmpeg.path"));
try {
movieCoder.init();
} catch (Exception e) {
throw new ServletException("unable to locate movie helpers (mplayer and ffmpeg)", e);
}
lastCacheFlushTime = System.currentTimeMillis();
}
private void log4jInit(String resource) {
try {
Properties props = new Properties();
props.load(getClass().getResourceAsStream(resource));
PropertyConfigurator.configure(props);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void destroy() {
log.info("Shutting down Album");
}
static Pattern ROOT_MATCHER = Pattern.compile("^/(\\w+)");
Root rootFor(String pathInfo) {
if (pathInfo == null) {
return null;
}
Matcher m = ROOT_MATCHER.matcher(pathInfo);
if (!m.find()) {
return null;
}
return roots.get(m.group(1));
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
doGet(req, res);
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
req.setAttribute("assets", req.getContextPath() + "/assets");
req.setAttribute("req", req);
req.setAttribute("base", req.getContextPath() + req.getServletPath());
String pathInfo = req.getPathInfo();
// help the user get to the top level page
if (pathInfo == null || "/".equals(pathInfo)) {
Root root = roots.values().stream().findFirst().orElse(null);
String u = req.getContextPath() + "/album/" + root.getName() + ".album";
res.sendRedirect(u);
return;
}
if ("/_roots.json".equals(pathInfo)) {
res.setContentType("application/json");
res.setCharacterEncoding("UTF-8");
ObjectMapper json = new ObjectMapper();
ArrayNode arr = json.createArrayNode();
roots.values().stream().map(r -> r.getName()).forEach(arr::add);
json.writeValue(res.getOutputStream(), arr);
return;
}
long now = System.currentTimeMillis();
if (now > nextCacheRefresh) {
roots.values().forEach(Root::flushCache);
long minute = 60 * 1000L;
nextCacheRefresh = now + minute;
}
try {
Root root = rootFor(pathInfo);
if (root == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
req.setAttribute("mapper", new Mapper(root.base));
if (pathInfo.endsWith(".search")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".search".length());
root.handleSearch(req, res, (DirectoryEntry)root.resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".album")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".album".length());
root.handleAlbum(req, res, (DirectoryEntry)root.resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".photo")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length());
root.handlePhoto(req, res, (FileEntry)root.resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".frame")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".frame".length());
root.handleMovieFrame(req, res, (FileEntry)root.resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".movie")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".movie".length());
root.handleMovie(req, res, (FileEntry)root.resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".edit")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".edit".length());
root.handleEdit(req, res, (FileEntry)root.resolveEntry(pathInfo));
return;
}
if (pathInfo.endsWith(".json")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".json".length());
DirectoryEntry directoryEntry = (DirectoryEntry)root.resolveEntry(pathInfo);
if (directoryEntry == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
root.handleJson(req, res, directoryEntry);
return;
}
if (pathInfo.endsWith(".cache")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - ".cache".length());
root.handleCache(req, res, (DirectoryEntry)root.resolveEntry(pathInfo));
return;
}
File file = new File(root.base.getParentFile(), pathInfo);
if (!file.canRead()) {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
String size = req.getParameter("size");
if (size != null) {
FileEntry e = (FileEntry)root.resolve(file);
root.procesScaledImageRequest(req, res, file, e.getThumbnail(), size);
return;
}
} catch (Exception e) {
throw new ServletException("sadness", e);
}
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
boolean etagMatches(HttpServletRequest req, String fileEtag) {
String cacheControl = req.getHeader("Cache-Control");
if ("no-cache".equals(cacheControl)) {
return false;
}
String reqEtag = req.getHeader("If-None-Match");
if (reqEtag != null) {
return reqEtag.equals(fileEtag);
}
return false;
}
boolean notModified(HttpServletRequest req, File f) {
String cacheControl = req.getHeader("Cache-Control");
if ("no-cache".equals(cacheControl)) {
return false;
}
long reqDate = req.getDateHeader("If-Modified-Since");
long fDate = f.lastModified();
return reqDate > 0 && fDate > 0 && fDate <= reqDate;
}
@Override
@ -621,40 +741,6 @@ public class AlbumServlet
return "Display of org.forkalsrud.album";
}
/**
* maps files to URIs relative to servlet, e.g. /home/joe/photos/holiday/arrival.jpg -> /photos/holiday/arrival.jpg
* assuming base is /home/joe/photos
*/
public class Mapper {
public String map(File file) {
StringBuilder buf = new StringBuilder();
return appendFile(buf, file).toString();
}
StringBuilder appendFile(StringBuilder buf, File file) {
if (file == null) {
return buf;
}
if (base.equals(file.getAbsoluteFile())) {
return buf.append("/").append(base.getName());
} else {
return appendFile(buf, file.getParentFile()).append('/').append(file.getName());
}
}
Calendar cal = Calendar.getInstance();
public String year(Date d) {
if (d == null) {
return "";
}
cal.setTime(d);
return String.valueOf(cal.get(Calendar.YEAR));
}
}
}
// eof

View file

@ -21,11 +21,8 @@ import java.util.concurrent.TimeUnit;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import org.forkalsrud.album.exif.Dimension;
@ -91,7 +88,7 @@ public class PictureScaler {
try {
return scalePictureReally(file, thumbnail, size);
} catch (Exception e) {
log.error("sadness", e);
log.error("sadness: " + file.getAbsolutePath(), e);
return new CachedImage();
}
}
@ -208,7 +205,7 @@ public class PictureScaler {
xform.scale(flipX[idx], flipY[idx]);
xform.translate(-img.getWidth() / 2d, -img.getHeight() / 2d);
int imgType = img.getType();
int imgType = img.getType() > 0 ? img.getType() : BufferedImage.TYPE_INT_RGB;
BufferedImage buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), imgType);
Graphics2D g2 = buf2.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

View file

@ -84,7 +84,7 @@
<script type="text/javascript" src="/photo/assets/render.js"></script>
</head>
<body>
<form action="/photo/album/search" method="get"><input name="q" value=""></form>
<form id="search" action="/photo/album/search" method="get"><input name="q" value=""></form>
<h1 id="titleBar" style="height: 32;"><img class="nav" width="26" height="32" src="/photo/assets/left-inactive.png"><img class="nav" width="26" height="32" src="/photo/assets/up-inactive.png"><img class="nav" width="26" height="32" src="/photo/assets/right-inactive.png">&nbsp;</h1>
<hr/>
</body>

View file

@ -205,7 +205,7 @@ $D(function() {
</script>
</head>
<body>
<form action="${base}/search" method="get"><input name="q" value="$!search"></form>
<form id="search" action="${entry.name}.search" method="get"><input name="q" value="$!search"></form>
#macro(navlink $entry)${base}$mapper.map(${entry.getPath()}).#if($entry.isFile())photo#{else}album#end#end
#macro(navbutton $entry $direction)#if($entry)<a href="#navlink($entry)"><img class="nav" width="26" height="32" src="${assets}/${direction}.png"/></a>#else<img class="nav" width="26" height="32" src="${assets}/${direction}-inactive.png"/>#end#end
<h1>#navbutton(${entry.prev()} "left")#navbutton(${entry.parent()} "up")#navbutton(${entry.next()} "right") $entry.name</h1>

View file

@ -77,8 +77,9 @@
$(document).ready(function() {
function formatTitle(title, currentArray, currentIndex, currentOpts) {
var captionElement = document.getElementById(title);
return captionElement.innerHTML;
return $(currentArray[currentIndex]).parent().parent().children("p").html();
// var captionElement = document.getElementById(title);
// return captionElement.innerHTML;
}
var selectedImg = window.location.search;
var selectedPos = undefined;
@ -91,6 +92,7 @@ $(document).ready(function() {
});
}
var gallery = $("a.ss").fancybox({
'type' : 'image',
'titlePosition' : 'inside',
'transitionIn' : 'elastic',
'transitionOut' : 'elastic',
@ -128,7 +130,7 @@ $(document).ready(function() {
#else
<div class="imgborder"><a class="dir" href="#navlink($en)" title="$!en.caption"><img class="picture" src="${base}${thpath}?size=${thmb}" border="0" width="${dim.width}" height="${dim.height}"/></a></div>
#end
<p id="${en.name}" class="caption">$!en.caption</p>
<p class="caption">$!en.caption</p>
</div>
#end
</body>

View file

@ -14,6 +14,10 @@ $(function() {
}
}
$("#search").each(function (id, el) {
el.action = location.pathname.replace(/\.album(\?|$)/, '.search');
});
$.getJSON(location.pathname.replace(/\.album(\?|$)/, '.json'), function(data, textStatus) {
$("#name").text(data.name);