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>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>2.3.2</version>
|
<version>2.3.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>1.8</source>
|
<source>8</source>
|
||||||
<target>1.8</target>
|
<target>8</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
||||||
boolean childrenLoaded = false;
|
boolean childrenLoaded = false;
|
||||||
Comparator<Entry> sort = null;
|
Comparator<Entry> sort = null;
|
||||||
Date earliest = null;
|
Date earliest = null;
|
||||||
|
boolean groupByYear;
|
||||||
|
|
||||||
public DirectoryEntry(ServiceApi api, File file) {
|
public DirectoryEntry(ServiceApi api, File file) {
|
||||||
super(file);
|
super(file);
|
||||||
|
|
@ -197,6 +198,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.earliest = oldest;
|
this.earliest = oldest;
|
||||||
|
this.groupByYear = "year".equalsIgnoreCase(props.getProperty("group"));
|
||||||
if (thumbnail == null && !children.isEmpty()) {
|
if (thumbnail == null && !children.isEmpty()) {
|
||||||
setThumbnail(children.get(0).getThumbnail());
|
setThumbnail(children.get(0).getThumbnail());
|
||||||
}
|
}
|
||||||
|
|
@ -212,7 +214,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean groupByYear() {
|
public boolean groupByYear() {
|
||||||
return parent == null;
|
return this.groupByYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
package org.forkalsrud.album.exif;
|
package org.forkalsrud.album.exif;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.forkalsrud.album.db.DirectoryDatabase;
|
import org.forkalsrud.album.db.DirectoryDatabase;
|
||||||
import org.forkalsrud.album.video.MovieCoder;
|
import org.forkalsrud.album.video.MovieMetadataGenerator;
|
||||||
|
|
||||||
public class DirectoryEntryFactory implements DirectoryEntry.ServiceApi {
|
public class DirectoryEntryFactory implements DirectoryEntry.ServiceApi {
|
||||||
|
|
||||||
private DirectoryDatabase dirDb;
|
private DirectoryDatabase dirDb;
|
||||||
private DirectoryMetadataGenerator generator;
|
private DirectoryMetadataGenerator generator;
|
||||||
|
private MovieMetadataGenerator movieGenerator;
|
||||||
|
|
||||||
public DirectoryEntryFactory(DirectoryDatabase dirDb, MovieCoder movieCoder) {
|
public DirectoryEntryFactory(DirectoryDatabase dirDb) throws IOException, InterruptedException {
|
||||||
this.dirDb = dirDb;
|
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.ImageMetadataReader;
|
||||||
import com.drew.imaging.ImageProcessingException;
|
import com.drew.imaging.ImageProcessingException;
|
||||||
import com.drew.imaging.jpeg.JpegProcessingException;
|
|
||||||
import org.forkalsrud.album.db.DirectoryProps;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.drew.imaging.jpeg.JpegMetadataReader;
|
|
||||||
import com.drew.metadata.Directory;
|
import com.drew.metadata.Directory;
|
||||||
import com.drew.metadata.Metadata;
|
import com.drew.metadata.Metadata;
|
||||||
import com.drew.metadata.MetadataException;
|
import com.drew.metadata.MetadataException;
|
||||||
|
|
@ -39,10 +37,10 @@ public class DirectoryMetadataGenerator {
|
||||||
final static String CACHE_FILE = "cache.properties";
|
final static String CACHE_FILE = "cache.properties";
|
||||||
final static String OVERRIDE_FILE = "album.properties";
|
final static String OVERRIDE_FILE = "album.properties";
|
||||||
|
|
||||||
MovieCoder movieCoder;
|
MovieMetadataGenerator movieMetadataGenerator;
|
||||||
|
|
||||||
public DirectoryMetadataGenerator(MovieCoder movieCoder) {
|
public DirectoryMetadataGenerator(MovieMetadataGenerator generator) {
|
||||||
this.movieCoder = movieCoder;
|
this.movieMetadataGenerator = generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -91,7 +89,7 @@ public class DirectoryMetadataGenerator {
|
||||||
addPropsForFile(props, f, p);
|
addPropsForFile(props, f, p);
|
||||||
}
|
}
|
||||||
} else if (name.endsWith(".mov") || name.endsWith(".MOV") || name.endsWith(".mp4") || name.endsWith(".m4v") || name.endsWith(".avi")) {
|
} 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);
|
addPropsForFile(props, f, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package org.forkalsrud.album.exif;
|
package org.forkalsrud.album.exif;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
public class SearchEngine {
|
public class SearchEngine {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ public class SearchResults extends EntryWithChildren<SearchEntry> {
|
||||||
|
|
||||||
protected SearchResults(DirectoryEntry root) {
|
protected SearchResults(DirectoryEntry root) {
|
||||||
super(root);
|
super(root);
|
||||||
|
this.next = root.next;
|
||||||
|
this.prev = root.prev;
|
||||||
|
this.parent = root.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addMatch(Entry entry) {
|
public void addMatch(Entry entry) {
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,10 @@ import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.LineNumberReader;
|
import java.io.LineNumberReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.forkalsrud.album.db.Chunk;
|
import org.forkalsrud.album.db.Chunk;
|
||||||
import org.forkalsrud.album.db.MovieDatabase;
|
import org.forkalsrud.album.db.MovieDatabase;
|
||||||
import org.forkalsrud.album.exif.Dimension;
|
import org.forkalsrud.album.exif.Dimension;
|
||||||
|
|
@ -28,14 +23,11 @@ public class MovieCoder {
|
||||||
|
|
||||||
private String ffmpegExecutable;
|
private String ffmpegExecutable;
|
||||||
private String mplayerExecutable;
|
private String mplayerExecutable;
|
||||||
private String exiftoolExecutable;
|
|
||||||
private PictureScaler pictureScaler;
|
private PictureScaler pictureScaler;
|
||||||
private MovieDatabase movieDb;
|
|
||||||
private HashMap<String, EncodingProcess> currentEncodings = new HashMap<String, EncodingProcess>();
|
private HashMap<String, EncodingProcess> currentEncodings = new HashMap<String, EncodingProcess>();
|
||||||
|
|
||||||
public MovieCoder(PictureScaler pictureScaler, MovieDatabase movieDb) {
|
public MovieCoder(PictureScaler pictureScaler) {
|
||||||
this.pictureScaler = pictureScaler;
|
this.pictureScaler = pictureScaler;
|
||||||
this.movieDb = movieDb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() throws Exception {
|
public void init() throws Exception {
|
||||||
|
|
@ -45,175 +37,6 @@ public class MovieCoder {
|
||||||
this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg");
|
this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg");
|
||||||
}
|
}
|
||||||
this.mplayerExecutable = util.findExecutableInShellPath("mplayer");
|
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
|
* @return
|
||||||
*/
|
*/
|
||||||
private synchronized EncodingProcess submitEncodingJob(File file,
|
private synchronized EncodingProcess submitEncodingJob(File file,
|
||||||
Thumbnail thumbnail, Dimension targetSize, String key) {
|
Thumbnail thumbnail, Dimension targetSize, String key, MovieDatabase movieDb) {
|
||||||
EncodingProcess ep;
|
EncodingProcess ep;
|
||||||
ep = new EncodingProcess(file, thumbnail, targetSize);
|
ep = new EncodingProcess(file, thumbnail, targetSize, movieDb);
|
||||||
currentEncodings.put(key, ep);
|
currentEncodings.put(key, ep);
|
||||||
new Thread(ep).start();
|
new Thread(ep).start();
|
||||||
return ep;
|
return ep;
|
||||||
|
|
@ -295,8 +118,10 @@ public class MovieCoder {
|
||||||
private long fileTimestamp;
|
private long fileTimestamp;
|
||||||
private int orientation;
|
private int orientation;
|
||||||
private volatile boolean done = false;
|
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.file = file;
|
||||||
this.fileTimestamp = file.lastModified();
|
this.fileTimestamp = file.lastModified();
|
||||||
this.targetSize = size.even();
|
this.targetSize = size.even();
|
||||||
|
|
@ -305,6 +130,7 @@ public class MovieCoder {
|
||||||
extraMeta.setDuration(thumbnail.getDuration());
|
extraMeta.setDuration(thumbnail.getDuration());
|
||||||
this.filter = new FlvFilter(this, extraMeta);
|
this.filter = new FlvFilter(this, extraMeta);
|
||||||
this.orientation = thumbnail.getOrientation();
|
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
|
// 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 {
|
try {
|
||||||
grabStream(file, thumbnail, size).stream(out);
|
grabStream(file, thumbnail, size, movieDb).stream(out);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("stream fail", 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();
|
Dimension targetSize = thumbnail.getSize().scale(size).even();
|
||||||
String key = key(file, targetSize);
|
String key = key(file, targetSize);
|
||||||
|
|
@ -563,9 +389,9 @@ public class MovieCoder {
|
||||||
}
|
}
|
||||||
// If neither we need to start the encoding process
|
// If neither we need to start the encoding process
|
||||||
if (chunk == null && ep == null) {
|
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 Chunk chunk;
|
||||||
private String key;
|
private String key;
|
||||||
private boolean done = false;
|
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.key = key;
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
this.chunk = chunk0;
|
this.chunk = chunk0;
|
||||||
|
this.movieDb = movieDb;
|
||||||
// Range requests can hook in here
|
// Range requests can hook in here
|
||||||
// if we have chunk metadata in chunk0 we can use that to compute the first
|
// 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
|
// 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.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.*;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.logging.Handler;
|
import java.util.logging.Handler;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.LogRecord;
|
import java.util.logging.LogRecord;
|
||||||
import java.util.logging.Logger;
|
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.RequestDispatcher;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
|
|
@ -21,6 +22,8 @@ import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
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.apache.log4j.PropertyConfigurator;
|
||||||
import org.forkalsrud.album.db.DirectoryDatabase;
|
import org.forkalsrud.album.db.DirectoryDatabase;
|
||||||
import org.forkalsrud.album.db.DirectoryProps;
|
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.Environment;
|
||||||
import com.sleepycat.je.EnvironmentConfig;
|
import com.sleepycat.je.EnvironmentConfig;
|
||||||
|
|
||||||
|
|
||||||
public class AlbumServlet
|
public class AlbumServlet
|
||||||
extends HttpServlet
|
extends HttpServlet
|
||||||
{
|
{
|
||||||
|
|
@ -94,219 +98,142 @@ public class AlbumServlet
|
||||||
"com.sleepycat.je.recovery.RecoveryManager");
|
"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;
|
File base;
|
||||||
String basePrefix;
|
String basePrefix;
|
||||||
PictureScaler pictureScaler;
|
DirectoryEntryFactory dirEntryFactory;
|
||||||
long lastCacheFlushTime;
|
|
||||||
private Environment environment;
|
private Environment environment;
|
||||||
ThumbnailDatabase thumbDb;
|
ThumbnailDatabase thumbDb;
|
||||||
DirectoryDatabase dirDb;
|
DirectoryDatabase dirDb;
|
||||||
MovieDatabase movieDb;
|
MovieDatabase movieDb;
|
||||||
Entry cachedRootNode = null;
|
Entry cachedRootNode = null;
|
||||||
MovieCoder movieCoder;
|
|
||||||
DirectoryEntryFactory dirEntryFactory;
|
|
||||||
long nextCacheRefresh;
|
|
||||||
|
|
||||||
@Override
|
public Root(String name, File base, File dbDir) {
|
||||||
public void init()
|
this.name = name;
|
||||||
throws ServletException
|
this.base = base;
|
||||||
{
|
this.basePrefix = "/" + base.getName();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 environmentConfig = new EnvironmentConfig();
|
||||||
environmentConfig.setAllowCreate(true);
|
environmentConfig.setAllowCreate(true);
|
||||||
environmentConfig.setTransactional(true);
|
environmentConfig.setTransactional(true);
|
||||||
environment = new Environment(dbDir, environmentConfig);
|
this.environment = new Environment(dbDir, environmentConfig);
|
||||||
|
|
||||||
thumbDb = new ThumbnailDatabase(environment);
|
this.thumbDb = new ThumbnailDatabase(environment);
|
||||||
dirDb = new DirectoryDatabase(environment);
|
this.dirDb = new DirectoryDatabase(environment);
|
||||||
movieDb = new MovieDatabase(environment);
|
this.movieDb = new MovieDatabase(environment);
|
||||||
|
|
||||||
|
|
||||||
pictureScaler = new PictureScaler();
|
|
||||||
|
|
||||||
movieCoder = new MovieCoder(pictureScaler, movieDb);
|
|
||||||
movieCoder.setFfmpegPath(props.getProperty("ffmpeg.path"));
|
|
||||||
try {
|
try {
|
||||||
movieCoder.init();
|
dirEntryFactory = new DirectoryEntryFactory(dirDb);
|
||||||
} catch (Exception e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
throw new ServletException("unable to locate movie helpers (mplayer and ffmpeg)", e);
|
log.error("initialization of " + name, 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
|
public void flushCache() {
|
||||||
|
cachedRootNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
log.info("Shutting down Album");
|
|
||||||
dirDb.destroy();
|
dirDb.destroy();
|
||||||
thumbDb.destroy();
|
thumbDb.destroy();
|
||||||
environment.close();
|
environment.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public String getName() {
|
||||||
public void doPost(HttpServletRequest req, HttpServletResponse res)
|
return name;
|
||||||
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());
|
|
||||||
req.setAttribute("mapper", new Mapper());
|
|
||||||
String pathInfo = req.getPathInfo();
|
|
||||||
|
|
||||||
// help the user get to the top level page
|
|
||||||
if (pathInfo == null || "/".equals(pathInfo)) {
|
Entry resolveEntry(String pathInfo) {
|
||||||
String u = req.getContextPath() + "/album/" + base.getName() + ".album";
|
|
||||||
res.sendRedirect(u);
|
if (pathInfo == null || "/".equals(pathInfo)) return resolve(base);
|
||||||
return;
|
return resolve(new File(base.getParentFile(), pathInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("/_roots.json".equals(pathInfo)) {
|
Entry resolve(File file) {
|
||||||
res.setContentType("application/json");
|
|
||||||
res.setCharacterEncoding("UTF-8");
|
|
||||||
PrintWriter out = res.getWriter();
|
|
||||||
out.append("[\"").append(base.getName()).append("\"]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
if (base.equals(file.getAbsoluteFile())) {
|
||||||
if (now > nextCacheRefresh) {
|
synchronized (this) {
|
||||||
cachedRootNode = null;
|
if (cachedRootNode == null) {
|
||||||
long minute = 60 * 1000L;
|
cachedRootNode = dirEntryFactory.getEntry(file, null);
|
||||||
nextCacheRefresh = now + minute;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
return cachedRootNode;
|
||||||
if (pathInfo.startsWith(basePrefix)) {
|
|
||||||
pathInfo = pathInfo.substring(basePrefix.length());
|
|
||||||
} else if (pathInfo.equals("/search")) {
|
|
||||||
handleSearch(req, res, (DirectoryEntry)resolveEntry("/"));
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
res.sendError(HttpServletResponse.SC_NOT_FOUND, "pathinfo=" + pathInfo);
|
return ((DirectoryEntry)resolve(file.getParentFile())).get(file);
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
void handlePhoto(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
|
||||||
res.setContentType("text/html");
|
res.setContentType("text/html");
|
||||||
|
|
@ -343,7 +270,8 @@ public class AlbumServlet
|
||||||
String key = file.getPath() + ":" + secondNo + ":" + size;
|
String key = file.getPath() + ":" + secondNo + ":" + size;
|
||||||
CachedImage cimg = thumbDb.load(key);
|
CachedImage cimg = thumbDb.load(key);
|
||||||
if (cimg != null) {
|
if (cimg != null) {
|
||||||
if (cimg.lastModified == file.lastModified()) {
|
long fileTs = file.lastModified();
|
||||||
|
if (cimg.lastModified >= fileTs) {
|
||||||
log.info("cache hit on " + key);
|
log.info("cache hit on " + key);
|
||||||
} else {
|
} else {
|
||||||
log.info(" " + key + " has changed so cache entry wil be refreshed");
|
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.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
|
||||||
// res.setHeader("Cache-control", "no-cache");
|
// res.setHeader("Cache-control", "no-cache");
|
||||||
res.setContentType("video/x-flv");
|
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) {
|
} catch (Exception ex) {
|
||||||
log.error("darn", ex);
|
log.error("darn", ex);
|
||||||
throw new RuntimeException("sadness", ex);
|
throw new RuntimeException("sadness", ex);
|
||||||
|
|
@ -398,7 +326,7 @@ public class AlbumServlet
|
||||||
|
|
||||||
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
|
||||||
try {
|
try {
|
||||||
Mapper mapper = new Mapper();
|
Mapper mapper = new Mapper(base);
|
||||||
res.setContentType("application/json");
|
res.setContentType("application/json");
|
||||||
res.setCharacterEncoding("UTF-8");
|
res.setCharacterEncoding("UTF-8");
|
||||||
PrintWriter out = res.getWriter();
|
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 {
|
void procesScaledImageRequest(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException {
|
||||||
|
|
||||||
if (notModified(req, file)) {
|
if (notModified(req, file)) {
|
||||||
|
|
@ -590,30 +479,261 @@ public class AlbumServlet
|
||||||
res.setContentLength(cimg.bits.length);
|
res.setContentLength(cimg.bits.length);
|
||||||
res.getOutputStream().write(cimg.bits);
|
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) {
|
Map<String, Root> roots = new HashMap<String, Root>();
|
||||||
if (cachedRootNode == null) {
|
|
||||||
cachedRootNode = dirEntryFactory.getEntry(file, null);
|
|
||||||
|
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 {
|
} 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
|
@Override
|
||||||
|
|
@ -621,40 +741,6 @@ public class AlbumServlet
|
||||||
return "Display of org.forkalsrud.album";
|
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
|
// eof
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,8 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.imageio.IIOImage;
|
import javax.imageio.IIOImage;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.imageio.ImageReadParam;
|
|
||||||
import javax.imageio.ImageReader;
|
|
||||||
import javax.imageio.ImageWriteParam;
|
import javax.imageio.ImageWriteParam;
|
||||||
import javax.imageio.ImageWriter;
|
import javax.imageio.ImageWriter;
|
||||||
import javax.imageio.stream.ImageInputStream;
|
|
||||||
import javax.imageio.stream.ImageOutputStream;
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
|
|
||||||
import org.forkalsrud.album.exif.Dimension;
|
import org.forkalsrud.album.exif.Dimension;
|
||||||
|
|
@ -91,7 +88,7 @@ public class PictureScaler {
|
||||||
try {
|
try {
|
||||||
return scalePictureReally(file, thumbnail, size);
|
return scalePictureReally(file, thumbnail, size);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("sadness", e);
|
log.error("sadness: " + file.getAbsolutePath(), e);
|
||||||
return new CachedImage();
|
return new CachedImage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +205,7 @@ public class PictureScaler {
|
||||||
xform.scale(flipX[idx], flipY[idx]);
|
xform.scale(flipX[idx], flipY[idx]);
|
||||||
xform.translate(-img.getWidth() / 2d, -img.getHeight() / 2d);
|
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);
|
BufferedImage buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), imgType);
|
||||||
Graphics2D g2 = buf2.createGraphics();
|
Graphics2D g2 = buf2.createGraphics();
|
||||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
<script type="text/javascript" src="/photo/assets/render.js"></script>
|
<script type="text/javascript" src="/photo/assets/render.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<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/>
|
<hr/>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ $D(function() {
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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(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
|
#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>
|
<h1>#navbutton(${entry.prev()} "left")#navbutton(${entry.parent()} "up")#navbutton(${entry.next()} "right") $entry.name</h1>
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,9 @@
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
function formatTitle(title, currentArray, currentIndex, currentOpts) {
|
function formatTitle(title, currentArray, currentIndex, currentOpts) {
|
||||||
var captionElement = document.getElementById(title);
|
return $(currentArray[currentIndex]).parent().parent().children("p").html();
|
||||||
return captionElement.innerHTML;
|
// var captionElement = document.getElementById(title);
|
||||||
|
// return captionElement.innerHTML;
|
||||||
}
|
}
|
||||||
var selectedImg = window.location.search;
|
var selectedImg = window.location.search;
|
||||||
var selectedPos = undefined;
|
var selectedPos = undefined;
|
||||||
|
|
@ -91,6 +92,7 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var gallery = $("a.ss").fancybox({
|
var gallery = $("a.ss").fancybox({
|
||||||
|
'type' : 'image',
|
||||||
'titlePosition' : 'inside',
|
'titlePosition' : 'inside',
|
||||||
'transitionIn' : 'elastic',
|
'transitionIn' : 'elastic',
|
||||||
'transitionOut' : 'elastic',
|
'transitionOut' : 'elastic',
|
||||||
|
|
@ -128,7 +130,7 @@ $(document).ready(function() {
|
||||||
#else
|
#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>
|
<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
|
#end
|
||||||
<p id="${en.name}" class="caption">$!en.caption</p>
|
<p class="caption">$!en.caption</p>
|
||||||
</div>
|
</div>
|
||||||
#end
|
#end
|
||||||
</body>
|
</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) {
|
$.getJSON(location.pathname.replace(/\.album(\?|$)/, '.json'), function(data, textStatus) {
|
||||||
|
|
||||||
$("#name").text(data.name);
|
$("#name").text(data.name);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue