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:
parent
cb2a20ed2c
commit
9fac9425ce
14 changed files with 703 additions and 586 deletions
4
pom.xml
4
pom.xml
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -93,18 +97,399 @@ public class AlbumServlet
|
|||
"com.sleepycat.je.cleaner.FileProcessor",
|
||||
"com.sleepycat.je.recovery.RecoveryManager");
|
||||
}
|
||||
|
||||
File base;
|
||||
String basePrefix;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
DirectoryEntryFactory dirEntryFactory;
|
||||
|
||||
private Environment environment;
|
||||
ThumbnailDatabase thumbDb;
|
||||
DirectoryDatabase dirDb;
|
||||
MovieDatabase movieDb;
|
||||
Entry cachedRootNode = null;
|
||||
|
||||
public Root(String name, File base, File dbDir) {
|
||||
this.name = name;
|
||||
this.base = base;
|
||||
this.basePrefix = "/" + base.getName();
|
||||
|
||||
|
||||
EnvironmentConfig environmentConfig = new EnvironmentConfig();
|
||||
environmentConfig.setAllowCreate(true);
|
||||
environmentConfig.setTransactional(true);
|
||||
this.environment = new Environment(dbDir, environmentConfig);
|
||||
|
||||
this.thumbDb = new ThumbnailDatabase(environment);
|
||||
this.dirDb = new DirectoryDatabase(environment);
|
||||
this.movieDb = new MovieDatabase(environment);
|
||||
|
||||
|
||||
try {
|
||||
dirEntryFactory = new DirectoryEntryFactory(dirDb);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
log.error("initialization of " + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void flushCache() {
|
||||
cachedRootNode = null;
|
||||
}
|
||||
|
||||
|
||||
public void destroy() {
|
||||
dirDb.destroy();
|
||||
thumbDb.destroy();
|
||||
environment.close();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Entry resolveEntry(String pathInfo) {
|
||||
|
||||
if (pathInfo == null || "/".equals(pathInfo)) return resolve(base);
|
||||
return resolve(new File(base.getParentFile(), pathInfo));
|
||||
}
|
||||
|
||||
Entry resolve(File file) {
|
||||
|
||||
if (base.equals(file.getAbsoluteFile())) {
|
||||
synchronized (this) {
|
||||
if (cachedRootNode == null) {
|
||||
cachedRootNode = dirEntryFactory.getEntry(file, null);
|
||||
}
|
||||
}
|
||||
return cachedRootNode;
|
||||
} else {
|
||||
return ((DirectoryEntry)resolve(file.getParentFile())).get(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
void handlePhoto(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
|
||||
res.setContentType("text/html");
|
||||
req.setAttribute("entry", entry);
|
||||
req.setAttribute("thmb", new Integer(800));
|
||||
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
|
||||
rd.forward(req, res);
|
||||
}
|
||||
|
||||
void handleAlbum(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception {
|
||||
res.setContentType("text/html");
|
||||
req.setAttribute("entry", entry);
|
||||
req.setAttribute("thmb", new Integer(250));
|
||||
req.setAttribute("full", new Integer(800));
|
||||
req.setAttribute("D", "$");
|
||||
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/ng.html");
|
||||
rd.forward(req, res);
|
||||
}
|
||||
|
||||
void handleMovieFrame(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
|
||||
|
||||
File file = entry.getPath();
|
||||
if (notModified(req, file)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on date)");
|
||||
return;
|
||||
}
|
||||
int secondNo = 3;
|
||||
String size = req.getParameter("size");
|
||||
if (size == null) {
|
||||
size = "250";
|
||||
}
|
||||
String key = file.getPath() + ":" + secondNo + ":" + size;
|
||||
CachedImage cimg = thumbDb.load(key);
|
||||
if (cimg != null) {
|
||||
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");
|
||||
cimg = null;
|
||||
}
|
||||
}
|
||||
if (cimg == null) {
|
||||
try {
|
||||
cimg = movieCoder.extractFrame(file, secondNo, entry.getThumbnail(), size);
|
||||
thumbDb.store(key, cimg);
|
||||
log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries");
|
||||
} catch (Exception e) {
|
||||
//e.fillInStackTrace();
|
||||
throw new RuntimeException("sadness", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", file.lastModified());
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
res.setContentType(cimg.mimeType);
|
||||
res.setContentLength(cimg.bits.length);
|
||||
res.getOutputStream().write(cimg.bits);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("sadness", ex);
|
||||
}
|
||||
}
|
||||
|
||||
void handleMovie(HttpServletRequest req, HttpServletResponse res, FileEntry entry) {
|
||||
|
||||
File file = entry.getPath();
|
||||
if (notModified(req, file)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on date)");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String size = req.getParameter("size");
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", entry.getPath().lastModified());
|
||||
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(), movieDb);
|
||||
} catch (Exception ex) {
|
||||
log.error("darn", ex);
|
||||
throw new RuntimeException("sadness", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
||||
try {
|
||||
Mapper mapper = new Mapper(base);
|
||||
res.setContentType("application/json");
|
||||
res.setCharacterEncoding("UTF-8");
|
||||
PrintWriter out = res.getWriter();
|
||||
out.println("{");
|
||||
out.println(" \"name\": " + jsStr(entry.getName()) + ",");
|
||||
if (entry.parent() != null && "dir".equals(entry.parent().getType())) {
|
||||
out.println(" \"parent\": " + jsStr(mapper.map(entry.parent().getPath())) + ",");
|
||||
}
|
||||
if (entry.prev() != null && "dir".equals(entry.prev().getType())) {
|
||||
out.println(" \"prev\": " + jsStr(mapper.map(entry.prev().getPath())) + ",");
|
||||
}
|
||||
if (entry.next() != null && "dir".equals(entry.next().getType())) {
|
||||
out.println(" \"next\": " + jsStr(mapper.map(entry.next().getPath())) + ",");
|
||||
}
|
||||
if (entry.groupByYear()) {
|
||||
out.println(" \"groupPrefix\": 4,");
|
||||
}
|
||||
out.println(" \"contents\": [");
|
||||
int n = 0;
|
||||
for (Entry e : entry.getContents()) {
|
||||
try {
|
||||
if (n++ > 0) out.println(",");
|
||||
out.println(" {");
|
||||
out.println(" \"name\": " + jsStr(e.getName()) + ",");
|
||||
out.println(" \"type\": " + jsStr(e.getType()) + ",");
|
||||
if ("dir".equals(e.getType())) {
|
||||
DirectoryEntry de = (DirectoryEntry)e;
|
||||
if (de.getEarliest() != null) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy");
|
||||
out.println(" \"earliest\": " + jsStr(sdf.format(de.getEarliest())) + ",");
|
||||
}
|
||||
}
|
||||
Thumbnail thumb = e.getThumbnail();
|
||||
if (thumb != null) {
|
||||
out.println(" \"path\": " + jsStr(mapper.map(thumb.getPath())) + ",");
|
||||
out.println(" \"thumbtype\": " + jsStr(thumb.getType()) + ",");
|
||||
out.println(" \"width\": " + thumb.getSize().getWidth() + ",");
|
||||
out.print(" \"height\": " + thumb.getSize().getHeight());
|
||||
}
|
||||
if (e.getCaption() != null) {
|
||||
out.println(",\n \"caption\": " + jsStr(e.getCaption()));
|
||||
} else {
|
||||
out.println();
|
||||
}
|
||||
out.print(" }");
|
||||
} catch (Exception ex) {
|
||||
throw new Exception(e.toString(), ex);
|
||||
}
|
||||
}
|
||||
out.println();
|
||||
out.println(" ]");
|
||||
out.println("}");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("sadness", e);
|
||||
}
|
||||
}
|
||||
|
||||
String jsStr(String in) {
|
||||
return in == null ? "null" : "\"" + in.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\"";
|
||||
}
|
||||
|
||||
void handleCache(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
||||
if (entry == null) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
DirectoryProps props = entry.getCache();
|
||||
if (props == null) {
|
||||
props = new DirectoryProps();
|
||||
}
|
||||
try {
|
||||
res.setContentType("text/plain");
|
||||
res.setCharacterEncoding("UTF-8");
|
||||
PrintWriter out = res.getWriter();
|
||||
out.println("# cache timestamp: " + props.getTimestamp());
|
||||
out.println("# dir timestamp: " + entry.getPath().lastModified());
|
||||
out.println();
|
||||
props.store(out, "");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("sadness", e);
|
||||
}
|
||||
}
|
||||
void handleEdit(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
|
||||
String value = req.getParameter("value");
|
||||
if (value != null) {
|
||||
File propertyFile = new File(entry.getPath().getParent(), "album.properties");
|
||||
Properties props = new Properties();
|
||||
if (propertyFile.exists()) {
|
||||
FileInputStream fis = new FileInputStream(propertyFile);
|
||||
props.load(fis);
|
||||
fis.close();
|
||||
}
|
||||
props.setProperty("file." + entry.getName() + ".caption", value);
|
||||
FileOutputStream fos = new FileOutputStream(propertyFile);
|
||||
props.store(fos, "online editor");
|
||||
fos.close();
|
||||
res.setContentType("text/html");
|
||||
res.getWriter().println(HtmlUtils.htmlEscape(value));
|
||||
cachedRootNode = null;
|
||||
return;
|
||||
}
|
||||
res.setContentType("text/html");
|
||||
req.setAttribute("entry", entry);
|
||||
req.setAttribute("thmb", new Integer(640));
|
||||
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/edit.vm");
|
||||
rd.forward(req, res);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void procesScaledImageRequest(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException {
|
||||
|
||||
if (notModified(req, file)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on date)");
|
||||
return;
|
||||
}
|
||||
String fileEtag = thumbnail.getEtag() + "-" + size;
|
||||
if (etagMatches(req, fileEtag)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on etag)");
|
||||
return;
|
||||
}
|
||||
|
||||
String key = file.getPath() + ":" + size;
|
||||
|
||||
CachedImage cimg = thumbDb.load(key);
|
||||
if (cimg != null) {
|
||||
if (cimg.lastModified == file.lastModified()) {
|
||||
log.info("cache hit on " + key);
|
||||
} else {
|
||||
log.info(" " + key + " has changed so cache entry wil be refreshed");
|
||||
cimg = null;
|
||||
}
|
||||
}
|
||||
if (cimg == null) {
|
||||
cimg = pictureScaler.scalePicture(file, thumbnail, size);
|
||||
thumbDb.store(key, cimg);
|
||||
log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries");
|
||||
}
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", file.lastModified());
|
||||
res.setHeader("ETag", fileEtag);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
res.setContentType(cimg.mimeType);
|
||||
res.setContentLength(cimg.bits.length);
|
||||
res.getOutputStream().write(cimg.bits);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Map<String, Root> roots = new HashMap<String, Root>();
|
||||
|
||||
|
||||
PictureScaler pictureScaler;
|
||||
long lastCacheFlushTime;
|
||||
private Environment environment;
|
||||
ThumbnailDatabase thumbDb;
|
||||
DirectoryDatabase dirDb;
|
||||
MovieDatabase movieDb;
|
||||
Entry cachedRootNode = null;
|
||||
MovieCoder movieCoder;
|
||||
DirectoryEntryFactory dirEntryFactory;
|
||||
|
||||
long lastCacheFlushTime;
|
||||
long nextCacheRefresh;
|
||||
|
||||
@Override
|
||||
|
|
@ -131,36 +516,30 @@ public class AlbumServlet
|
|||
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);
|
||||
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 {
|
||||
dbDir = new File(System.getProperty("java.io.tmpdir"), "album");
|
||||
if (dbDir.isDirectory() && dbDir.canWrite()) {
|
||||
for (File f : dbDir.listFiles()) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
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));
|
||||
});
|
||||
}
|
||||
dbDir.mkdirs();
|
||||
|
||||
EnvironmentConfig environmentConfig = new EnvironmentConfig();
|
||||
environmentConfig.setAllowCreate(true);
|
||||
environmentConfig.setTransactional(true);
|
||||
environment = new Environment(dbDir, environmentConfig);
|
||||
|
||||
thumbDb = new ThumbnailDatabase(environment);
|
||||
dirDb = new DirectoryDatabase(environment);
|
||||
movieDb = new MovieDatabase(environment);
|
||||
|
||||
|
||||
pictureScaler = new PictureScaler();
|
||||
|
||||
movieCoder = new MovieCoder(pictureScaler, movieDb);
|
||||
movieCoder = new MovieCoder(pictureScaler);
|
||||
movieCoder.setFfmpegPath(props.getProperty("ffmpeg.path"));
|
||||
try {
|
||||
movieCoder.init();
|
||||
|
|
@ -168,7 +547,6 @@ public class AlbumServlet
|
|||
throw new ServletException("unable to locate movie helpers (mplayer and ffmpeg)", e);
|
||||
}
|
||||
|
||||
dirEntryFactory = new DirectoryEntryFactory(dirDb, movieCoder);
|
||||
|
||||
lastCacheFlushTime = System.currentTimeMillis();
|
||||
}
|
||||
|
|
@ -186,11 +564,25 @@ public class AlbumServlet
|
|||
@Override
|
||||
public void destroy() {
|
||||
log.info("Shutting down Album");
|
||||
dirDb.destroy();
|
||||
thumbDb.destroy();
|
||||
environment.close();
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
|
@ -206,12 +598,12 @@ public class AlbumServlet
|
|||
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";
|
||||
Root root = roots.values().stream().findFirst().orElse(null);
|
||||
String u = req.getContextPath() + "/album/" + root.getName() + ".album";
|
||||
res.sendRedirect(u);
|
||||
return;
|
||||
}
|
||||
|
|
@ -219,77 +611,83 @@ public class AlbumServlet
|
|||
if ("/_roots.json".equals(pathInfo)) {
|
||||
res.setContentType("application/json");
|
||||
res.setCharacterEncoding("UTF-8");
|
||||
PrintWriter out = res.getWriter();
|
||||
out.append("[\"").append(base.getName()).append("\"]");
|
||||
|
||||
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) {
|
||||
cachedRootNode = null;
|
||||
roots.values().forEach(Root::flushCache);
|
||||
long minute = 60 * 1000L;
|
||||
nextCacheRefresh = now + minute;
|
||||
}
|
||||
|
||||
try {
|
||||
if (pathInfo.startsWith(basePrefix)) {
|
||||
pathInfo = pathInfo.substring(basePrefix.length());
|
||||
} else if (pathInfo.equals("/search")) {
|
||||
handleSearch(req, res, (DirectoryEntry)resolveEntry("/"));
|
||||
Root root = rootFor(pathInfo);
|
||||
if (root == null) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
} else {
|
||||
res.sendError(HttpServletResponse.SC_NOT_FOUND, "pathinfo=" + pathInfo);
|
||||
}
|
||||
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());
|
||||
handleAlbum(req, res, (DirectoryEntry)resolveEntry(pathInfo));
|
||||
root.handleAlbum(req, res, (DirectoryEntry)root.resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".photo")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length());
|
||||
handlePhoto(req, res, (FileEntry)resolveEntry(pathInfo));
|
||||
root.handlePhoto(req, res, (FileEntry)root.resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".frame")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".frame".length());
|
||||
handleMovieFrame(req, res, (FileEntry)resolveEntry(pathInfo));
|
||||
root.handleMovieFrame(req, res, (FileEntry)root.resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".movie")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".movie".length());
|
||||
handleMovie(req, res, (FileEntry)resolveEntry(pathInfo));
|
||||
root.handleMovie(req, res, (FileEntry)root.resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".edit")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".edit".length());
|
||||
handleEdit(req, res, (FileEntry)resolveEntry(pathInfo));
|
||||
root.handleEdit(req, res, (FileEntry)root.resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".json")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".json".length());
|
||||
DirectoryEntry directoryEntry = (DirectoryEntry)resolveEntry(pathInfo);
|
||||
DirectoryEntry directoryEntry = (DirectoryEntry)root.resolveEntry(pathInfo);
|
||||
if (directoryEntry == null) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
handleJson(req, res, directoryEntry);
|
||||
root.handleJson(req, res, directoryEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathInfo.endsWith(".cache")) {
|
||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".cache".length());
|
||||
handleCache(req, res, (DirectoryEntry)resolveEntry(pathInfo));
|
||||
root.handleCache(req, res, (DirectoryEntry)root.resolveEntry(pathInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(base, pathInfo);
|
||||
File file = new File(root.base.getParentFile(), pathInfo);
|
||||
if (!file.canRead()) {
|
||||
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
return;
|
||||
|
|
@ -298,8 +696,8 @@ public class AlbumServlet
|
|||
String size = req.getParameter("size");
|
||||
if (size != null) {
|
||||
|
||||
FileEntry e = (FileEntry)resolve(file);
|
||||
procesScaledImageRequest(req, res, file, e.getThumbnail(), size);
|
||||
FileEntry e = (FileEntry)root.resolve(file);
|
||||
root.procesScaledImageRequest(req, res, file, e.getThumbnail(), size);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
|
@ -308,222 +706,6 @@ public class AlbumServlet
|
|||
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
|
||||
void handlePhoto(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
|
||||
res.setContentType("text/html");
|
||||
req.setAttribute("entry", entry);
|
||||
req.setAttribute("thmb", new Integer(800));
|
||||
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
|
||||
rd.forward(req, res);
|
||||
}
|
||||
|
||||
void handleAlbum(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception {
|
||||
res.setContentType("text/html");
|
||||
req.setAttribute("entry", entry);
|
||||
req.setAttribute("thmb", new Integer(250));
|
||||
req.setAttribute("full", new Integer(800));
|
||||
req.setAttribute("D", "$");
|
||||
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/ng.html");
|
||||
rd.forward(req, res);
|
||||
}
|
||||
|
||||
void handleMovieFrame(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
|
||||
|
||||
File file = entry.getPath();
|
||||
if (notModified(req, file)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on date)");
|
||||
return;
|
||||
}
|
||||
int secondNo = 3;
|
||||
String size = req.getParameter("size");
|
||||
if (size == null) {
|
||||
size = "250";
|
||||
}
|
||||
String key = file.getPath() + ":" + secondNo + ":" + size;
|
||||
CachedImage cimg = thumbDb.load(key);
|
||||
if (cimg != null) {
|
||||
if (cimg.lastModified == file.lastModified()) {
|
||||
log.info("cache hit on " + key);
|
||||
} else {
|
||||
log.info(" " + key + " has changed so cache entry wil be refreshed");
|
||||
cimg = null;
|
||||
}
|
||||
}
|
||||
if (cimg == null) {
|
||||
try {
|
||||
cimg = movieCoder.extractFrame(file, secondNo, entry.getThumbnail(), size);
|
||||
thumbDb.store(key, cimg);
|
||||
log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries");
|
||||
} catch (Exception e) {
|
||||
//e.fillInStackTrace();
|
||||
throw new RuntimeException("sadness", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", file.lastModified());
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
res.setContentType(cimg.mimeType);
|
||||
res.setContentLength(cimg.bits.length);
|
||||
res.getOutputStream().write(cimg.bits);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("sadness", ex);
|
||||
}
|
||||
}
|
||||
|
||||
void handleMovie(HttpServletRequest req, HttpServletResponse res, FileEntry entry) {
|
||||
|
||||
File file = entry.getPath();
|
||||
if (notModified(req, file)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on date)");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String size = req.getParameter("size");
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", entry.getPath().lastModified());
|
||||
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());
|
||||
} catch (Exception ex) {
|
||||
log.error("darn", ex);
|
||||
throw new RuntimeException("sadness", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
||||
try {
|
||||
Mapper mapper = new Mapper();
|
||||
res.setContentType("application/json");
|
||||
res.setCharacterEncoding("UTF-8");
|
||||
PrintWriter out = res.getWriter();
|
||||
out.println("{");
|
||||
out.println(" \"name\": " + jsStr(entry.getName()) + ",");
|
||||
if (entry.parent() != null && "dir".equals(entry.parent().getType())) {
|
||||
out.println(" \"parent\": " + jsStr(mapper.map(entry.parent().getPath())) + ",");
|
||||
}
|
||||
if (entry.prev() != null && "dir".equals(entry.prev().getType())) {
|
||||
out.println(" \"prev\": " + jsStr(mapper.map(entry.prev().getPath())) + ",");
|
||||
}
|
||||
if (entry.next() != null && "dir".equals(entry.next().getType())) {
|
||||
out.println(" \"next\": " + jsStr(mapper.map(entry.next().getPath())) + ",");
|
||||
}
|
||||
if (entry.groupByYear()) {
|
||||
out.println(" \"groupPrefix\": 4,");
|
||||
}
|
||||
out.println(" \"contents\": [");
|
||||
int n = 0;
|
||||
for (Entry e : entry.getContents()) {
|
||||
try {
|
||||
if (n++ > 0) out.println(",");
|
||||
out.println(" {");
|
||||
out.println(" \"name\": " + jsStr(e.getName()) + ",");
|
||||
out.println(" \"type\": " + jsStr(e.getType()) + ",");
|
||||
if ("dir".equals(e.getType())) {
|
||||
DirectoryEntry de = (DirectoryEntry)e;
|
||||
if (de.getEarliest() != null) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy");
|
||||
out.println(" \"earliest\": " + jsStr(sdf.format(de.getEarliest())) + ",");
|
||||
}
|
||||
}
|
||||
Thumbnail thumb = e.getThumbnail();
|
||||
if (thumb != null) {
|
||||
out.println(" \"path\": " + jsStr(mapper.map(thumb.getPath())) + ",");
|
||||
out.println(" \"thumbtype\": " + jsStr(thumb.getType()) + ",");
|
||||
out.println(" \"width\": " + thumb.getSize().getWidth() + ",");
|
||||
out.print(" \"height\": " + thumb.getSize().getHeight());
|
||||
}
|
||||
if (e.getCaption() != null) {
|
||||
out.println(",\n \"caption\": " + jsStr(e.getCaption()));
|
||||
} else {
|
||||
out.println();
|
||||
}
|
||||
out.print(" }");
|
||||
} catch (Exception ex) {
|
||||
throw new Exception(e.toString(), ex);
|
||||
}
|
||||
}
|
||||
out.println();
|
||||
out.println(" ]");
|
||||
out.println("}");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("sadness", e);
|
||||
}
|
||||
}
|
||||
|
||||
String jsStr(String in) {
|
||||
return in == null ? "null" : "\"" + in.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\"";
|
||||
}
|
||||
|
||||
void handleCache(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
||||
if (entry == null) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
DirectoryProps props = entry.getCache();
|
||||
if (props == null) {
|
||||
props = new DirectoryProps();
|
||||
}
|
||||
try {
|
||||
res.setContentType("text/plain");
|
||||
res.setCharacterEncoding("UTF-8");
|
||||
PrintWriter out = res.getWriter();
|
||||
out.println("# cache timestamp: " + props.getTimestamp());
|
||||
out.println("# dir timestamp: " + entry.getPath().lastModified());
|
||||
out.println();
|
||||
props.store(out, "");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("sadness", e);
|
||||
}
|
||||
}
|
||||
void handleEdit(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
|
||||
String value = req.getParameter("value");
|
||||
if (value != null) {
|
||||
File propertyFile = new File(entry.getPath().getParent(), "album.properties");
|
||||
Properties props = new Properties();
|
||||
if (propertyFile.exists()) {
|
||||
FileInputStream fis = new FileInputStream(propertyFile);
|
||||
props.load(fis);
|
||||
fis.close();
|
||||
}
|
||||
props.setProperty("file." + entry.getName() + ".caption", value);
|
||||
FileOutputStream fos = new FileOutputStream(propertyFile);
|
||||
props.store(fos, "online editor");
|
||||
fos.close();
|
||||
res.setContentType("text/html");
|
||||
res.getWriter().println(HtmlUtils.htmlEscape(value));
|
||||
cachedRootNode = null;
|
||||
return;
|
||||
}
|
||||
res.setContentType("text/html");
|
||||
req.setAttribute("entry", entry);
|
||||
req.setAttribute("thmb", new Integer(640));
|
||||
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/edit.vm");
|
||||
rd.forward(req, res);
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
|
|
@ -550,111 +732,15 @@ public class AlbumServlet
|
|||
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)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on date)");
|
||||
return;
|
||||
}
|
||||
String fileEtag = thumbnail.getEtag() + "-" + size;
|
||||
if (etagMatches(req, fileEtag)) {
|
||||
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
log.info(file.getName() + " not modified (based on etag)");
|
||||
return;
|
||||
}
|
||||
|
||||
String key = file.getPath() + ":" + size;
|
||||
|
||||
CachedImage cimg = thumbDb.load(key);
|
||||
if (cimg != null) {
|
||||
if (cimg.lastModified == file.lastModified()) {
|
||||
log.info("cache hit on " + key);
|
||||
} else {
|
||||
log.info(" " + key + " has changed so cache entry wil be refreshed");
|
||||
cimg = null;
|
||||
}
|
||||
}
|
||||
if (cimg == null) {
|
||||
cimg = pictureScaler.scalePicture(file, thumbnail, size);
|
||||
thumbDb.store(key, cimg);
|
||||
log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries");
|
||||
}
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
res.setDateHeader("Last-Modified", file.lastModified());
|
||||
res.setHeader("ETag", fileEtag);
|
||||
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||
res.setContentType(cimg.mimeType);
|
||||
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);
|
||||
}
|
||||
}
|
||||
return cachedRootNode;
|
||||
} else {
|
||||
return ((DirectoryEntry)resolve(file.getParentFile())).get(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getServletInfo() {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"> </h1>
|
||||
<hr/>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue