Compare commits

..

No commits in common. "9dd452fe3645e33cb1f24a196db765c68b388397" and "9851784fdea3bd146be1a0a5730a69660ff72b87" have entirely different histories.

17 changed files with 1387 additions and 1461 deletions

View file

@ -16,8 +16,8 @@
Create a file: `~/forkalsrud.org/photo.properties` and for each named "root" Create a file: `~/forkalsrud.org/photo.properties` and for each named "root"
(`photo` in the example below), point the `base` property and `dbdir` properties (`photo` in the example below), point the `base` property and `dbdir` properties
to the directory holding the photos, and the directory to be populated with to the direcory holding the photos, and the diractory to be populated with
BerkeleyDB files to your photo folder: BerkelyDB files to your photo folder:
root.photo.base=/home/photo root.photo.base=/home/photo
root.photo.dbdir=/home/resin/web/db root.photo.dbdir=/home/resin/web/db

18
pom.xml
View file

@ -119,7 +119,7 @@
<dependency> <dependency>
<groupId>com.drewnoakes</groupId> <groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId> <artifactId>metadata-extractor</artifactId>
<version>2.19.0</version> <version>2.18.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
@ -247,22 +247,6 @@
<artifactId>pdfbox-io</artifactId> <artifactId>pdfbox-io</artifactId>
<version>3.0.0</version> <version>3.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>jbig2-imageio</artifactId>
<version>3.0.4</version>
</dependency>
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-jpeg2000</artifactId>
<version>1.4.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>

View file

@ -93,9 +93,7 @@ public class ThumbnailDatabase extends TupleBinding<CachedImage> {
img.mimeType = in.readString(); img.mimeType = in.readString();
int lobLen = in.readInt(); int lobLen = in.readInt();
img.bits = new byte[lobLen]; img.bits = new byte[lobLen];
if (lobLen > 0) {
in.read(img.bits, 0, lobLen); in.read(img.bits, 0, lobLen);
}
return img; return img;
} }
@ -106,12 +104,8 @@ public class ThumbnailDatabase extends TupleBinding<CachedImage> {
out.writeInt(1); // version 1 out.writeInt(1); // version 1
out.writeLong(img.lastModified); out.writeLong(img.lastModified);
out.writeString(img.mimeType); out.writeString(img.mimeType);
if (img.bits != null) {
out.writeInt(img.bits.length); out.writeInt(img.bits.length);
out.write(img.bits); out.write(img.bits);
} else {
out.writeInt(0);
}
} }
} }

View file

@ -367,7 +367,7 @@ public class Editor
int findRegion(int ex, int ey) { int findRegion(int ex, int ey) {
Rectangle r = roi.getScreenRectangle(); Rectangle r = roi.getScreenRectangle();
int c; int c = 8;
if (within(ex, r.x + r.width)) { if (within(ex, r.x + r.width)) {
if (within(ey, r.y + r.height)) { if (within(ey, r.y + r.height)) {

View file

@ -26,14 +26,7 @@ public class Dimension {
} }
public Dimension(String s) { public Dimension(String s) {
String[] coords; String[] coords = s.split("x");
if (s.contains("x")) {
coords = s.split("x");
} else if (s.contains(" ")) {
coords = s.split(" ");
} else {
throw new RuntimeException("Bad dimension: " + s);
}
this.w = Integer.parseInt(coords[0]); this.w = Integer.parseInt(coords[0]);
this.h = Integer.parseInt(coords[1]); this.h = Integer.parseInt(coords[1]);
} }

View file

@ -8,8 +8,11 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import org.forkalsrud.album.db.DirectoryDatabase; import org.forkalsrud.album.db.DirectoryDatabase;
@ -24,14 +27,14 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
public interface ServiceApi { public interface ServiceApi {
DirectoryDatabase getDirectoryDatabase(); public DirectoryDatabase getDirectoryDatabase();
DirectoryMetadataGenerator getMetadataGenerator(); public DirectoryMetadataGenerator getMetadataGenerator();
} }
ServiceApi services; ServiceApi services;
DirectoryProps cache; DirectoryProps cache;
boolean childrenLoaded = false; boolean childrenLoaded = false;
Comparator<Entry> ordering = null; Comparator<Entry> sort = null;
Date earliest = null; Date earliest = null;
boolean groupByYear; boolean groupByYear;
@ -126,7 +129,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
private void sort() { private void sort() {
children.sort(ordering); Collections.sort(children, sort);
} }
@ -136,19 +139,28 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
String coverFileName = props.getProperty("cover"); String coverFileName = props.getProperty("cover");
String caption = props.getProperty("caption"); String caption = props.getProperty("caption");
setCaption(caption); setCaption(caption);
ordering = ComparatorFactory.getSort(props.getProperty("sort")); sort = ComparatorFactory.getSort(props.getProperty("sort"));
HashMap<String, Entry> entryMap = new HashMap<String, Entry>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
for (String key : props.stringPropertyNames()) { Date oldest = new Date(file.lastModified());
Iterator<Object> i = props.keySet().iterator();
while (i.hasNext()) {
String key = (String)i.next();
if (key.startsWith("file.") && key.endsWith(".dimensions")) { if (key.startsWith("file.") && key.endsWith(".dimensions")) {
String name = key.substring("file.".length(), key.length() - ".dimensions".length()); String name = key.substring("file.".length(), key.length() - ".dimensions".length());
if (!entryMap.containsKey(name)) {
File f = new File(file, name); File f = new File(file, name);
String type = props.getProperty("file." + name + ".type", "image"); String type = props.getProperty("file." + name + ".type", "image");
FileEntry entry = new FileEntry(this, f, type); FileEntry entry = new FileEntry(this, f, type);
Thumbnail thumbnail = new Thumbnail(f, type); Thumbnail thumbnail = new Thumbnail(f, type);
entry.setCaption(props.getProperty("file." + name + ".caption")); entry.setCaption(props.getProperty("file." + name + ".caption"));
Date fileDate = sdf.parse(props.getProperty("file." + name + ".captureDate")); Date fileDate = sdf.parse(props.getProperty("file." + name + ".captureDate"));
if (fileDate.before(oldest)) {
oldest = fileDate;
}
entry.setDate(fileDate); entry.setDate(fileDate);
thumbnail.setSize(new Dimension(props.getProperty("file." + name + ".dimensions"))); thumbnail.setSize(new Dimension(props.getProperty("file." + name + ".dimensions")));
thumbnail.setOrientation(Integer.parseInt(props.getProperty("file." + name + ".orientation"))); thumbnail.setOrientation(Integer.parseInt(props.getProperty("file." + name + ".orientation")));
@ -157,7 +169,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
boolean hidden = Boolean.parseBoolean(props.getProperty("file." + name + ".hidden")); boolean hidden = Boolean.parseBoolean(props.getProperty("file." + name + ".hidden"));
if (!hidden) { if (!hidden) {
children.add(entry); children.add(entry);
if (name.equals(coverFileName)) { if (name != null && name.equals(coverFileName)) {
setThumbnail(thumbnail); setThumbnail(thumbnail);
} }
} }
@ -165,16 +177,19 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
if (duration != null) { if (duration != null) {
thumbnail.setDuration(duration); thumbnail.setDuration(duration);
} }
}
} else if (key.startsWith("dir.") && !key.endsWith(".hidden") && !key.endsWith(".caption")) { } else if (key.startsWith("dir.") && !key.endsWith(".hidden") && !key.endsWith(".caption")) {
String name = key.substring("dir.".length()); String name = key.substring("dir.".length());
boolean hidden = Boolean.parseBoolean(props.getProperty("dir." + name + ".hidden")); boolean hidden = Boolean.parseBoolean(props.getProperty("dir." + name + ".hidden"));
if (!hidden) { if (!hidden) {
DirectoryEntry dir = new DirectoryEntry(services, this, new File(file, name)); DirectoryEntry dir = new DirectoryEntry(services, this, new File(file, name));
if (name.equals(coverFileName)) { if (name != null && name.equals(coverFileName)) {
setThumbnail(dir.getThumbnail()); setThumbnail(dir.getThumbnail());
} }
Date fileDate = dir.getEarliest(); Date fileDate = dir.getEarliest();
dir.setDate(fileDate); if (fileDate != null && fileDate.before(oldest)) {
oldest = fileDate;
}
dir.setCaption(props.getProperty("dir." + name + ".caption")); dir.setCaption(props.getProperty("dir." + name + ".caption"));
if (!dir.getContents().isEmpty()) { if (!dir.getContents().isEmpty()) {
children.add(dir); children.add(dir);
@ -182,10 +197,7 @@ public class DirectoryEntry extends EntryWithChildren<Entry> {
} }
} }
} }
this.earliest = children.stream() this.earliest = oldest;
.map(Entry::getDate)
.min(Date::compareTo)
.orElse(new Date(file.lastModified()));
this.groupByYear = "year".equalsIgnoreCase(props.getProperty("group")); 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());

View file

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
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.exif.ExifIFD0Directory; import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory; import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.jpeg.JpegDirectory; import com.drew.metadata.jpeg.JpegDirectory;
@ -59,9 +60,6 @@ public class DirectoryMetadataGenerator {
private void generateFileEntries(File dir, Properties props) throws IOException, InterruptedException { private void generateFileEntries(File dir, Properties props) throws IOException, InterruptedException {
File[] files = dir.listFiles(); File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File f : files) { for (File f : files) {
if (f.length() == 0) { if (f.length() == 0) {
@ -136,6 +134,8 @@ public class DirectoryMetadataGenerator {
boolean hasOrientation = false; boolean hasOrientation = false;
boolean hasDim = false; boolean hasDim = false;
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
NumberFormat nf = new DecimalFormat("0");
Metadata metadata; Metadata metadata;
try { try {
@ -143,9 +143,6 @@ public class DirectoryMetadataGenerator {
} catch (ImageProcessingException e) { } catch (ImageProcessingException e) {
// not a JPEG file // not a JPEG file
return null; return null;
} catch (RuntimeException e) {
log.error("Unexpected error processing file " + f.getAbsolutePath(), e);
return null;
} }
try { try {
Directory exifDirectory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); Directory exifDirectory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
@ -208,7 +205,7 @@ public class DirectoryMetadataGenerator {
} }
private Date getExifDate(Directory exifDirectory, int tagName) { private Date getExifDate(Directory exifDirectory, int tagName) throws MetadataException {
String dateStr = exifDirectory.getString(tagName); String dateStr = exifDirectory.getString(tagName);
if (" : : : : ".equals(dateStr)) { if (" : : : : ".equals(dateStr)) {
@ -219,10 +216,16 @@ public class DirectoryMetadataGenerator {
private Dimension decodeImageForDimensions(File file) throws IOException { private Dimension decodeImageForDimensions(File file) throws IOException {
ImageReader reader = getImageReader(file); String suffix = null;
if (reader == null) { String name = file.getName();
if (name.indexOf('.') > 0) {
suffix = name.substring(name.lastIndexOf('.') + 1);
}
Iterator<ImageReader> readers = ImageIO.getImageReadersBySuffix(suffix);
if (!readers.hasNext()) {
return null; return null;
} }
ImageReader reader = readers.next();
ImageInputStream iis = ImageIO.createImageInputStream(file); ImageInputStream iis = ImageIO.createImageInputStream(file);
reader.setInput(iis, true); reader.setInput(iis, true);
ImageReadParam param = reader.getDefaultReadParam(); ImageReadParam param = reader.getDefaultReadParam();
@ -231,19 +234,6 @@ public class DirectoryMetadataGenerator {
return new Dimension(img.getWidth(), img.getHeight()); return new Dimension(img.getWidth(), img.getHeight());
} }
private ImageReader getImageReader(File file) {
String name = file.getName();
if (name.indexOf('.') < 0) {
return null;
}
String suffix = name.substring(name.lastIndexOf('.') + 1);
Iterator<ImageReader> readers = ImageIO.getImageReadersBySuffix(suffix);
if (!readers.hasNext()) {
return null;
}
ImageReader reader = readers.next();
return reader;
}
private Map<String, String> generatePdfProperties(File file) throws IOException { private Map<String, String> generatePdfProperties(File file) throws IOException {

View file

@ -13,7 +13,7 @@ import java.util.List;
*/ */
public class EntryWithChildren<T extends Entry> extends Entry { public class EntryWithChildren<T extends Entry> extends Entry {
protected List<T> children = new ArrayList<>(); protected List<T> children = new ArrayList<T>();
protected EntryWithChildren(Entry root) { protected EntryWithChildren(Entry root) {
super(root); super(root);

View file

@ -9,7 +9,14 @@ import java.util.HashMap;
public class SearchResults extends EntryWithChildren<SearchEntry> { public class SearchResults extends EntryWithChildren<SearchEntry> {
protected HashMap<File, SearchEntry> dupes = new HashMap<File, SearchEntry>(); protected HashMap<File, SearchEntry> dupes = new HashMap<File, SearchEntry>();
Comparator<SearchEntry> sort = (e1, e2) -> Integer.valueOf(e2.score).compareTo(e1.score); Comparator<SearchEntry> sort = new Comparator<SearchEntry>() {
@Override
public int compare(SearchEntry e1, SearchEntry e2) {
return Integer.valueOf(e2.score).compareTo(e1.score);
}
};
protected SearchResults(DirectoryEntry root) { protected SearchResults(DirectoryEntry root) {

View file

@ -0,0 +1,9 @@
package org.forkalsrud.album.video;
public interface EncodingProcessListener {
public abstract void chunkAvailable(int chunkNo);
public abstract void codingFinished(int lastCunkNo);
}

View file

@ -474,7 +474,7 @@ public class FlvMetadata {
seconds += 3600 * Double.parseDouble(durationStr.substring(0, pm)); seconds += 3600 * Double.parseDouble(durationStr.substring(0, pm));
} }
setDuration(seconds); setDuration(seconds);
} else if (durationStr != null) { } else {
setDuration(Double.parseDouble(durationStr)); setDuration(Double.parseDouble(durationStr));
} }
} }

View file

@ -6,12 +6,8 @@ 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.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.forkalsrud.album.db.Chunk; import org.forkalsrud.album.db.Chunk;
@ -23,12 +19,12 @@ import org.forkalsrud.album.web.PictureScaler;
public class MovieCoder { public class MovieCoder {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MovieCoder.class); private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MovieCoder.class);
private String ffmpegExecutable; private String ffmpegExecutable;
private String mplayerExecutable; private String mplayerExecutable;
private final PictureScaler pictureScaler; private PictureScaler pictureScaler;
private final HashMap<String, EncodingProcess> currentEncodings = new HashMap<>(); private HashMap<String, EncodingProcess> currentEncodings = new HashMap<String, EncodingProcess>();
public MovieCoder(PictureScaler pictureScaler) { public MovieCoder(PictureScaler pictureScaler) {
this.pictureScaler = pictureScaler; this.pictureScaler = pictureScaler;
@ -56,7 +52,7 @@ public class MovieCoder {
return temp; return temp;
} }
private void deleteTempDirectory(File dir) { public void deleteTempDirectory(File dir) {
for (File sub : dir.listFiles()) { for (File sub : dir.listFiles()) {
if (sub.isDirectory()) { if (sub.isDirectory()) {
deleteTempDirectory(sub); deleteTempDirectory(sub);
@ -78,7 +74,8 @@ public class MovieCoder {
p.getOutputStream().close(); p.getOutputStream().close();
log.debug(IOUtils.toString(p.getInputStream())); log.debug(IOUtils.toString(p.getInputStream()));
p.waitFor(); p.waitFor();
return pictureScaler.scalePicture(frame, thumbnail, size); CachedImage ci = pictureScaler.scalePicture(frame, thumbnail, size);
return ci;
} finally { } finally {
deleteTempDirectory(tmpDir); deleteTempDirectory(tmpDir);
} }
@ -87,11 +84,11 @@ public class MovieCoder {
/** /**
* @param file the file to encode * @param file
* @param thumbnail * @param thumbnail
* @param targetSize * @param targetSize
* @param key * @param key
* @return a handle to the encoding process * @return
*/ */
private synchronized EncodingProcess submitEncodingJob(File file, private synchronized EncodingProcess submitEncodingJob(File file,
Thumbnail thumbnail, Dimension targetSize, String key, MovieDatabase movieDb) { Thumbnail thumbnail, Dimension targetSize, String key, MovieDatabase movieDb) {
@ -108,19 +105,20 @@ public class MovieCoder {
class EncodingProcess implements Runnable, FlvFilter.FlvReceiver { class EncodingProcess implements Runnable, FlvFilter.FlvReceiver {
private final int chunkSize = 4 * 65536; private final int chunkSize = 4 * 65536;
private final File file; private File file;
private final Dimension targetSize; private Dimension targetSize;
private ArrayList<EncodingProcessListener> listeners = new ArrayList<EncodingProcessListener>();
private Chunk currentChunk = null; private Chunk currentChunk = null;
private int chunkPos; private int chunkPos;
private int remainingCapacity; private int remainingCapacity;
private final AtomicInteger chunkInProgress; private volatile int chunkInProgress = 0;
private volatile int chunkAvailable = 0; private volatile int chunkAvailable = 0;
private final FlvFilter filter; private FlvFilter filter;
private final String dbKey; private String dbKey;
private final long fileTimestamp; private long fileTimestamp;
private final int orientation; private int orientation;
private volatile boolean done = false; private volatile boolean done = false;
private final MovieDatabase movieDb; private MovieDatabase movieDb;
public EncodingProcess(File file, Thumbnail thumbnail, Dimension size, MovieDatabase movieDb) { public EncodingProcess(File file, Thumbnail thumbnail, Dimension size, MovieDatabase movieDb) {
@ -130,7 +128,6 @@ public class MovieCoder {
this.dbKey = key(file, targetSize); this.dbKey = key(file, targetSize);
FlvMetadata extraMeta = new FlvMetadata(); FlvMetadata extraMeta = new FlvMetadata();
extraMeta.setDuration(thumbnail.getDuration()); extraMeta.setDuration(thumbnail.getDuration());
this.chunkInProgress = new AtomicInteger(0);
this.filter = new FlvFilter(this, extraMeta); this.filter = new FlvFilter(this, extraMeta);
this.orientation = thumbnail.getOrientation(); this.orientation = thumbnail.getOrientation();
this.movieDb = movieDb; this.movieDb = movieDb;
@ -141,7 +138,7 @@ public class MovieCoder {
* http://rob.opendot.cl/index.php/useful-stuff/ffmpeg-x264-encoding-guide/ * http://rob.opendot.cl/index.php/useful-stuff/ffmpeg-x264-encoding-guide/
* http://rob.opendot.cl/index.php/useful-stuff/encoding-a-flv-video-for-embedded-web-playback/ * http://rob.opendot.cl/index.php/useful-stuff/encoding-a-flv-video-for-embedded-web-playback/
* *
* Assuming video in is 1280x720 pixels resolution, and we want to show 640x360, * Assuming video in is 1280x720 pixels resolution and we want to show 640x360
* and we want an output bitrate about 1 Mbit/sec * and we want an output bitrate about 1 Mbit/sec
* we can do a one pass encoding like this: * we can do a one pass encoding like this:
* *
@ -177,7 +174,25 @@ public class MovieCoder {
try { try {
ProcessBuilder pb = createTranscoderProcess(vf); ArrayList<String> command = new ArrayList<String>();
command.add(ffmpegExecutable);
command.add("-i");
command.add(file.getAbsolutePath());
command.add("-s");
command.add(targetSize.getWidth() + "x" + targetSize.getHeight());
command.add("-crf");
command.add("30");
command.add("-acodec"); command.add("libmp3lame");
command.add("-ar"); command.add("22050");
command.add("-vcodec"); command.add("libx264");
command.add("-g"); command.add("150");
if (vf != null) {
command.add("-vf");
command.add(vf);
}
command.add("-f"); command.add("flv");
command.add("-");
ProcessBuilder pb = new ProcessBuilder(command);
log.info(pb.command().toString()); log.info(pb.command().toString());
pb.redirectErrorStream(false); pb.redirectErrorStream(false);
@ -214,34 +229,6 @@ public class MovieCoder {
} }
} }
private ProcessBuilder createTranscoderProcess(String vf) {
ArrayList<String> command = new ArrayList<>();
command.add(ffmpegExecutable);
command.add("-i");
command.add(file.getAbsolutePath());
command.add("-s");
command.add(targetSize.getWidth() + "x" + targetSize.getHeight());
command.add("-crf");
command.add("30");
command.add("-acodec");
command.add("libmp3lame");
command.add("-ar");
command.add("22050");
command.add("-vcodec");
command.add("libx264");
command.add("-g");
command.add("150");
if (vf != null) {
command.add("-vf");
command.add(vf);
}
command.add("-f");
command.add("flv");
command.add("-");
ProcessBuilder pb = new ProcessBuilder(command);
return pb;
}
/** /**
* @return number of chunks available. * @return number of chunks available.
* -Integer.MAX_VALUE if something went very wrong * -Integer.MAX_VALUE if something went very wrong
@ -270,9 +257,9 @@ public class MovieCoder {
int len = data.length; int len = data.length;
Chunk chunk0 = new Chunk(fileTimestamp, len, 0); Chunk chunk0 = new Chunk(fileTimestamp, len, 0);
chunk0.bits = data; chunk0.bits = data;
log.info("Writing {} seq 0 ({}) {}", dbKey, chunkInProgress, data.length); log.info("Writing " + dbKey + " seq 0 (" + chunkInProgress + ") " + data.length);
movieDb.store(dbKey, 0, chunk0); movieDb.store(dbKey, 0, chunk0);
notifyListeners(chunkInProgress.get()); notifyListeners(chunkInProgress);
} }
@Override @Override
@ -302,7 +289,7 @@ public class MovieCoder {
private void startNewChunk() { private void startNewChunk() {
this.chunkInProgress.incrementAndGet(); this.chunkInProgress++;
this.currentChunk = new Chunk(fileTimestamp, chunkSize, 0); this.currentChunk = new Chunk(fileTimestamp, chunkSize, 0);
this.chunkPos = 0; this.chunkPos = 0;
this.remainingCapacity = chunkSize; this.remainingCapacity = chunkSize;
@ -310,11 +297,11 @@ public class MovieCoder {
private void endChunk() { private void endChunk() {
log.info("store chunk {}", chunkInProgress); log.info("store chunk " + chunkInProgress);
movieDb.store(dbKey, chunkInProgress.get(), currentChunk); movieDb.store(dbKey, chunkInProgress, currentChunk);
log.info("Writing {} seq {} ({}) {}", dbKey, chunkInProgress, chunkInProgress, currentChunk.bits.length); log.info("Writing " + dbKey + " seq " + chunkInProgress + " (" + chunkInProgress + ") " + currentChunk.bits.length);
currentChunk = null; currentChunk = null;
notifyListeners(chunkInProgress.get()); notifyListeners(chunkInProgress);
} }
private void endLastChunk() { private void endLastChunk() {
@ -325,16 +312,24 @@ public class MovieCoder {
// reallocate // reallocate
Chunk last = new Chunk(fileTimestamp, chunkPos, 0); Chunk last = new Chunk(fileTimestamp, chunkPos, 0);
System.arraycopy(currentChunk.bits, 0, last.bits, 0, chunkPos); System.arraycopy(currentChunk.bits, 0, last.bits, 0, chunkPos);
movieDb.store(dbKey, chunkInProgress.get(), last); movieDb.store(dbKey, chunkInProgress, last);
log.info("Writing {} seq {} ({}) {}", dbKey, chunkInProgress, chunkInProgress, last.bits.length); log.info("Writing " + dbKey + " seq " + chunkInProgress + " (" + chunkInProgress + ") " + last.bits.length);
currentChunk = null; currentChunk = null;
} }
public synchronized void addListener(EncodingProcessListener videoStreamer) {
listeners.add(videoStreamer);
} }
static class ErrorStreamPumper implements Runnable { public synchronized void removeListener(VideoStreamer videoStreamer) {
listeners.remove(videoStreamer);
}
}
private final InputStream is; class ErrorStreamPumper implements Runnable {
private final String name;
private InputStream is;
private String name;
public ErrorStreamPumper(String processName, InputStream is) { public ErrorStreamPumper(String processName, InputStream is) {
this.name = processName; this.name = processName;
@ -343,12 +338,12 @@ public class MovieCoder {
@Override @Override
public void run() { public void run() {
org.slf4j.Logger diagnostic = org.slf4j.LoggerFactory.getLogger(this.name); org.slf4j.Logger diag = org.slf4j.LoggerFactory.getLogger(this.name);
try { try {
LineNumberReader lnr = new LineNumberReader(new InputStreamReader(is)); LineNumberReader lnr = new LineNumberReader(new InputStreamReader(is));
String line; String line;
while ((line = lnr.readLine()) != null) { while ((line = lnr.readLine()) != null) {
diagnostic.info(line); diag.info(line);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("stderr?", e); log.error("stderr?", e);
@ -387,7 +382,7 @@ public class MovieCoder {
// If the file has been modified since our last encoding job finished // If the file has been modified since our last encoding job finished
if (chunk != null && ep == null) { if (chunk != null && ep == null) {
if (chunk.timestamp != file.lastModified()) { if (chunk.timestamp != file.lastModified()) {
log.info(" {} has changed so cache entry wil be refreshed", key); log.info(" " + key + " has changed so cache entry wil be refreshed");
movieDb.delete(key); movieDb.delete(key);
chunk = null; chunk = null;
} }
@ -403,11 +398,11 @@ public class MovieCoder {
class VideoStreamer { class VideoStreamer {
private int chunkNo = 0; private int chunkNo = 0;
private final EncodingProcess ep; private EncodingProcess ep;
private Chunk chunk; private Chunk chunk;
private final String key; private String key;
private boolean done = false; private boolean done = false;
private final MovieDatabase movieDb; private MovieDatabase movieDb;
private VideoStreamer(String key, EncodingProcess ep, Chunk chunk0, MovieDatabase movieDb) { private VideoStreamer(String key, EncodingProcess ep, Chunk chunk0, MovieDatabase movieDb) {
@ -429,11 +424,11 @@ public class MovieCoder {
while (!done) { while (!done) {
if (chunk == null) { if (chunk == null) {
log.info("Looking for {} - {}", key, chunkNo); log.info("Looking for " + key + " - " + chunkNo);
chunk = movieDb.load(key, chunkNo); chunk = movieDb.load(key, chunkNo);
} }
if (chunk != null) { if (chunk != null) {
log.info("Sending {}, {} bytes", chunkNo, chunk.bits.length); log.info("Sending " + chunkNo + ", " + chunk.bits.length + " bytes");
out.write(chunk.bits); out.write(chunk.bits);
chunk = null; chunk = null;
chunkNo++; chunkNo++;

View file

@ -20,13 +20,18 @@ import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.io.IOUtils;
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;
import org.forkalsrud.album.db.MovieDatabase; import org.forkalsrud.album.db.MovieDatabase;
import org.forkalsrud.album.db.ThumbnailDatabase; import org.forkalsrud.album.db.ThumbnailDatabase;
import org.forkalsrud.album.exif.*; import org.forkalsrud.album.exif.DirectoryEntry;
import org.forkalsrud.album.exif.DirectoryEntryFactory;
import org.forkalsrud.album.exif.Entry;
import org.forkalsrud.album.exif.FileEntry;
import org.forkalsrud.album.exif.SearchEngine;
import org.forkalsrud.album.exif.SearchResults;
import org.forkalsrud.album.exif.Thumbnail;
import org.forkalsrud.album.video.MovieCoder; import org.forkalsrud.album.video.MovieCoder;
import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.HtmlUtils;
@ -37,7 +42,7 @@ import com.sleepycat.je.EnvironmentConfig;
public class AlbumServlet public class AlbumServlet
extends HttpServlet extends HttpServlet
{ {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AlbumServlet.class); private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AlbumServlet.class);
static void addDummyLoggerFor(String... names) { static void addDummyLoggerFor(String... names) {
for (String name : names) for (String name : names)
@ -73,6 +78,7 @@ public class AlbumServlet
} }
}); });
} }
hh = ll.getHandlers();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -95,9 +101,9 @@ public class AlbumServlet
* maps files to URIs relative to servlet, e.g. /home/joe/photos/holiday/arrival.jpg -> /photos/holiday/arrival.jpg * 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 * assuming base is /home/joe/photos
*/ */
public static class Mapper { public class Mapper {
private final File base; private File base;
public Mapper(File base) { public Mapper(File base) {
this.base = base; this.base = base;
@ -108,7 +114,7 @@ public class AlbumServlet
return appendFile(buf, file).toString(); return appendFile(buf, file).toString();
} }
private StringBuilder appendFile(StringBuilder buf, File file) { StringBuilder appendFile(StringBuilder buf, File file) {
if (file == null) { if (file == null) {
return buf; return buf;
} }
@ -118,6 +124,16 @@ public class AlbumServlet
return appendFile(buf, file.getParentFile()).append('/').append(file.getName()); 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));
}
} }
@ -128,7 +144,7 @@ public class AlbumServlet
String basePrefix; String basePrefix;
DirectoryEntryFactory dirEntryFactory; DirectoryEntryFactory dirEntryFactory;
private final Environment environment; private Environment environment;
ThumbnailDatabase thumbDb; ThumbnailDatabase thumbDb;
DirectoryDatabase dirDb; DirectoryDatabase dirDb;
MovieDatabase movieDb; MovieDatabase movieDb;
@ -209,8 +225,8 @@ public class AlbumServlet
res.setContentType("text/html"); res.setContentType("text/html");
req.setAttribute("search", query); req.setAttribute("search", query);
req.setAttribute("entry", results); req.setAttribute("entry", results);
req.setAttribute("thmb", 250); req.setAttribute("thmb", new Integer(250));
req.setAttribute("full", 800); req.setAttribute("full", new Integer(800));
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm"); RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
rd.forward(req, res); rd.forward(req, res);
} }
@ -219,7 +235,7 @@ public class AlbumServlet
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");
req.setAttribute("entry", entry); req.setAttribute("entry", entry);
req.setAttribute("thmb", 800); req.setAttribute("thmb", new Integer(800));
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm"); RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm");
rd.forward(req, res); rd.forward(req, res);
} }
@ -227,20 +243,20 @@ public class AlbumServlet
void handleAlbum(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception { void handleAlbum(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception {
res.setContentType("text/html"); res.setContentType("text/html");
req.setAttribute("entry", entry); req.setAttribute("entry", entry);
req.setAttribute("thmb", 250); req.setAttribute("thmb", new Integer(250));
req.setAttribute("full", 800); req.setAttribute("full", new Integer(800));
req.setAttribute("D", "$"); req.setAttribute("D", "$");
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/ng.html"); RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/ng.html");
rd.forward(req, res); rd.forward(req, res);
} }
void handleMovieFrame(HttpServletRequest req, HttpServletResponse res, FileEntry entry) { void handleMovieFrame(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception {
File file = entry.getPath(); File file = entry.getPath();
if (notModified(req, file)) { if (notModified(req, file)) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
log.info("{} not modified (based on date)", file.getName()); log.info(file.getName() + " not modified (based on date)");
return; return;
} }
int secondNo = 3; int secondNo = 3;
@ -255,7 +271,7 @@ public class AlbumServlet
if (cimg.lastModified >= fileTs) { if (cimg.lastModified >= fileTs) {
// log.info("cache hit on " + key); // log.info("cache hit on " + key);
} else { } else {
log.info(" {} has changed so cache entry wil be refreshed", key); log.info(" " + key + " has changed so cache entry wil be refreshed");
cimg = null; cimg = null;
} }
} }
@ -263,7 +279,7 @@ public class AlbumServlet
try { try {
cimg = movieCoder.extractFrame(file, secondNo, entry.getThumbnail(), size); cimg = movieCoder.extractFrame(file, secondNo, entry.getThumbnail(), size);
thumbDb.store(key, cimg); thumbDb.store(key, cimg);
log.info(" {} added to the cache with size {} -- now {} entries", key, cimg.bits.length, thumbDb.size()); log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries");
} catch (Exception e) { } catch (Exception e) {
//e.fillInStackTrace(); //e.fillInStackTrace();
throw new RuntimeException("sadness", e); throw new RuntimeException("sadness", e);
@ -287,29 +303,11 @@ public class AlbumServlet
if (notModified(req, file)) { if (notModified(req, file)) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
log.info("{} not modified (based on date)", file.getName()); log.info(file.getName() + " not modified (based on date)");
return; return;
} }
String size = req.getParameter("size");
if (size == null) {
try (FileInputStream in = new FileInputStream(file);
OutputStream out = res.getOutputStream()) {
res.setStatus(HttpServletResponse.SC_OK);
res.setDateHeader("Last-Modified", entry.getPath().lastModified());
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
res.setContentType("video/mp4");
IOUtils.copyLarge(in, out);
} catch (IOException e) {
log("sadness", e);
}
return;
}
try { try {
String size = req.getParameter("size");
res.setStatus(HttpServletResponse.SC_OK); res.setStatus(HttpServletResponse.SC_OK);
res.setDateHeader("Last-Modified", entry.getPath().lastModified()); res.setDateHeader("Last-Modified", entry.getPath().lastModified());
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
@ -325,18 +323,6 @@ public class AlbumServlet
void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) { void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
try { try {
EntryWithChildren<?> contents;
String query = req.getParameter("q");
if (query != null && !"".equals(query.trim())) {
SearchEngine search = new SearchEngine(entry);
contents = search.search(query);
} else {
contents = entry;
}
Mapper mapper = new Mapper(base); Mapper mapper = new Mapper(base);
res.setContentType("application/json"); res.setContentType("application/json");
res.setCharacterEncoding("UTF-8"); res.setCharacterEncoding("UTF-8");
@ -345,23 +331,29 @@ public class AlbumServlet
ObjectNode root = json.createObjectNode(); ObjectNode root = json.createObjectNode();
root.put("name", entry.getName()); root.put("name", entry.getName());
addDir(root, entry.parent(), "parent", mapper); if (entry.parent() != null && "dir".equals(entry.parent().getType())) {
addDir(root, entry.prev(), "prev", mapper); root.put("parent", mapper.map(entry.parent().getPath()));
addDir(root, entry.next(), "next", mapper); }
if (entry.prev() != null && "dir".equals(entry.prev().getType())) {
root.put("prev", mapper.map(entry.prev().getPath()));
}
if (entry.next() != null && "dir".equals(entry.next().getType())) {
root.put("next", mapper.map(entry.next().getPath()));
}
if (entry.groupByYear()) { if (entry.groupByYear()) {
root.put("groupPrefix", 4); root.put("groupPrefix", 4);
} }
ArrayNode list = root.putArray("contents"); ArrayNode contents = root.putArray("contents");
for (Entry e : contents.getContents()) { for (Entry e : entry.getContents()) {
try { try {
ObjectNode item = list.addObject(); ObjectNode item = contents.addObject();
item.put("name", e.getName()); item.put("name", e.getName());
item.put("type", e.getType()); item.put("type", e.getType());
if ("dir".equals(e.getType())) { if ("dir".equals(e.getType())) {
if (e.getEarliest() != null) { DirectoryEntry de = (DirectoryEntry)e;
if (de.getEarliest() != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy");
item.put("earliest", sdf.format(e.getEarliest())); item.put("earliest", sdf.format(de.getEarliest()));
} }
} }
Thumbnail thumb = e.getThumbnail(); Thumbnail thumb = e.getThumbnail();
@ -385,14 +377,6 @@ public class AlbumServlet
} }
void addDir(ObjectNode root, Entry candidate, String label, Mapper mapper) {
if (candidate != null && "dir".equals(candidate.getType())) {
root.put(label, mapper.map(candidate.getPath()));
}
}
void handleCache(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) { void handleCache(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) {
if (entry == null) { if (entry == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND); res.setStatus(HttpServletResponse.SC_NOT_FOUND);
@ -435,7 +419,7 @@ public class AlbumServlet
} }
res.setContentType("text/html"); res.setContentType("text/html");
req.setAttribute("entry", entry); req.setAttribute("entry", entry);
req.setAttribute("thmb", 640); req.setAttribute("thmb", new Integer(640));
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/edit.vm"); RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/edit.vm");
rd.forward(req, res); rd.forward(req, res);
} }
@ -449,14 +433,14 @@ public class AlbumServlet
if (notModified(req, file)) { if (notModified(req, file)) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
log.info("{} not modified (based on date)", file.getName()); log.info(file.getName() + " not modified (based on date)");
return; return;
} }
String fileEtag = thumbnail.getEtag() + "-" + size; String fileEtag = thumbnail.getEtag() + "-" + size;
if (etagMatches(req, fileEtag)) { if (etagMatches(req, fileEtag)) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days
log.info("{} not modified (based on etag)", file.getName()); log.info(file.getName() + " not modified (based on etag)");
return; return;
} }
@ -467,15 +451,14 @@ public class AlbumServlet
if (cimg.lastModified == file.lastModified()) { if (cimg.lastModified == file.lastModified()) {
// log.info("cache hit on " + key); // log.info("cache hit on " + key);
} else { } else {
log.info(" {} has changed so cache entry wil be refreshed", key); log.info(" " + key + " has changed so cache entry wil be refreshed");
cimg = null; cimg = null;
} }
} }
if (cimg == null) { if (cimg == null) {
cimg = pictureScaler.scalePicture(file, thumbnail, size); cimg = pictureScaler.scalePicture(file, thumbnail, size);
thumbDb.store(key, cimg); thumbDb.store(key, cimg);
int byteSize = cimg.bits != null ? cimg.bits.length : 0; log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries");
log.info(" {} added to the cache with size {} -- now {} entries", key, byteSize, thumbDb.size());
} }
res.setStatus(HttpServletResponse.SC_OK); res.setStatus(HttpServletResponse.SC_OK);
res.setDateHeader("Last-Modified", file.lastModified()); res.setDateHeader("Last-Modified", file.lastModified());
@ -489,7 +472,7 @@ public class AlbumServlet
Map<String, Root> roots = new HashMap<>(); Map<String, Root> roots = new HashMap<String, Root>();
PictureScaler pictureScaler; PictureScaler pictureScaler;
@ -576,7 +559,7 @@ public class AlbumServlet
@Override @Override
public void destroy() { public void destroy() {
log.info("Shutting down Album"); log.info("Shutting down Album");
roots.values().forEach(Root::close); roots.values().forEach(r -> r.close());
} }
@ -615,15 +598,9 @@ public class AlbumServlet
// help the user get to the top level page // help the user get to the top level page
if (pathInfo == null || "/".equals(pathInfo)) { if (pathInfo == null || "/".equals(pathInfo)) {
String rootPath = roots.values().stream() Root root = roots.values().stream().findFirst().orElse(null);
.findFirst() String u = req.getContextPath() + "/album/" + root.getName() + ".album";
.map(r -> req.getContextPath() + "/album/" + r.getName() + ".album") res.sendRedirect(u);
.orElse(null);
if (rootPath != null) {
res.sendRedirect(rootPath);
} else {
res.setStatus(404);
}
return; return;
} }
@ -633,7 +610,7 @@ public class AlbumServlet
ObjectMapper json = new ObjectMapper(); ObjectMapper json = new ObjectMapper();
ArrayNode arr = json.createArrayNode(); ArrayNode arr = json.createArrayNode();
roots.values().stream().map(Root::getName).forEach(arr::add); roots.values().stream().map(r -> r.getName()).forEach(arr::add);
json.writeValue(res.getOutputStream(), arr); json.writeValue(res.getOutputStream(), arr);
return; return;
} }
@ -713,9 +690,8 @@ public class AlbumServlet
} }
String size = req.getParameter("size"); String size = req.getParameter("size");
if ("max".equals(size)) { if (size != null) {
getServletContext().getNamedDispatcher("file").forward(req, res);
} else if (size != null) {
FileEntry e = (FileEntry)root.resolve(file); FileEntry e = (FileEntry)root.resolve(file);
root.procesScaledImageRequest(req, res, file, e.getThumbnail(), size); root.procesScaledImageRequest(req, res, file, e.getThumbnail(), size);
return; return;

View file

@ -7,6 +7,7 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -25,7 +26,10 @@ import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSDocument;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.io.RandomAccessReadBufferedFile; import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
import org.apache.pdfbox.io.RandomAccessReadMemoryMappedFile;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
@ -43,10 +47,9 @@ public class PictureScaler {
public PictureScaler() { public PictureScaler() {
queue = new PriorityBlockingQueue<PictureRequest>(20, createPriorityComparator());
queue = new PriorityBlockingQueue<>(20, (o1, o2) -> Long.signum(o1.priority - o2.priority));
executor = Executors.newFixedThreadPool(4); executor = Executors.newFixedThreadPool(4);
outstandingRequests = new HashMap<>(); outstandingRequests = new HashMap<String, PictureRequest>();
} }
@ -116,6 +119,17 @@ public class PictureScaler {
} }
} }
Comparator<PictureRequest> createPriorityComparator() {
return new Comparator<PictureRequest>() {
@Override
public int compare(PictureRequest o1, PictureRequest o2) {
return Long.signum(o1.priority - o2.priority);
}
};
}
public CachedImage scalePicture(File file, Thumbnail thumbnail, String size) { public CachedImage scalePicture(File file, Thumbnail thumbnail, String size) {
String key = file.getPath() + ":" + size; String key = file.getPath() + ":" + size;
@ -167,13 +181,6 @@ public class PictureScaler {
} }
BufferedImage img = ImageIO.read(file); BufferedImage img = ImageIO.read(file);
if (img == null) {
CachedImage cimg = new CachedImage();
cimg.lastModified = file.lastModified();
cimg.mimeType = "image/jpeg";
cimg.bits = new byte[0];
return cimg;
}
// The orientation is about flipping and rotating. Here is what an 'F' looks like // The orientation is about flipping and rotating. Here is what an 'F' looks like
// on pictures with each orientation. // on pictures with each orientation.

View file

@ -6,9 +6,13 @@
<title>album</title> <title>album</title>
<script type="text/javascript" src="/photo/assets/jquery/jquery-3.7.1.min.js"></script> <script type="text/javascript" src="/photo/assets/jquery/jquery-3.7.1.min.js"></script>
<!--
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js"></script>
-->
<script type="text/javascript" src="/photo/assets/fancybox/jquery.fancybox-1.3.1.js"></script> <script type="text/javascript" src="/photo/assets/fancybox/jquery.fancybox-1.3.1.js"></script>
<script type="text/javascript" src="/photo/assets/fancybox/jquery.easing-1.3.pack.js"></script> <script type="text/javascript" src="/photo/assets/fancybox/jquery.easing-1.3.pack.js"></script>
<script type="text/javascript" src="/photo/assets/fancybox/jquery.mousewheel-3.0.2.pack.js"></script> <script type="text/javascript" src="/photo/assets/fancybox/jquery.mousewheel-3.0.2.pack.js"></script>
<script type="text/javascript" src="/photo/assets/flowplayer/flowplayer-3.0.3.min.js"></script>
<link rel="stylesheet" href="/photo/assets/fancybox/jquery.fancybox-1.3.1.css" type="text/css" media="screen" /> <link rel="stylesheet" href="/photo/assets/fancybox/jquery.fancybox-1.3.1.css" type="text/css" media="screen" />
@ -26,6 +30,7 @@
text-align: left; text-align: left;
} }
form { form {
float: right;
display: inline; display: inline;
} }
h2 { h2 {
@ -57,7 +62,7 @@
max-width: 275px; max-width: 275px;
overflow: hidden; overflow: hidden;
} }
img.imagepic, img.moviepic { img.imagepic {
box-shadow: 5px 5px 5px #777; box-shadow: 5px 5px 5px #777;
-webkit-box-shadow: 5px 5px 5px #777; -webkit-box-shadow: 5px 5px 5px #777;
-moz-box-shadow: 5px 5px 5px #777; -moz-box-shadow: 5px 5px 5px #777;
@ -76,26 +81,17 @@
vertical-align: middle; vertical-align: middle;
} }
#fancybox-left, #fancybox-right { #fancybox-left, #fancybox-right {
bottom: 70px; bottom: 30px;
} }
</style> </style>
<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 id="search" action="/photo/album/search" method="get"><!-- <form id="search" action="/photo/album/search" method="get"><input name="q" value=""></form>
--><h1 id="titleBar" style="height: 32;"><!-- <h1 id="titleBar" style="height: 32;"><!--
--><img id="arrowLeft" class="nav" width="26" height="32" src="/photo/assets/left-inactive.png"><!-- --><img class="nav" width="26" height="32" src="/photo/assets/left-inactive.png"><!--
--><img id="arrowUp" class="nav" width="26" height="32" src="/photo/assets/up-inactive.png"><!-- --><img class="nav" width="26" height="32" src="/photo/assets/up-inactive.png"><!--
--><img id="arrowRight" class="nav" width="26" height="32" src="/photo/assets/right-inactive.png"><!-- --><img class="nav" width="26" height="32" src="/photo/assets/right-inactive.png">&nbsp;</h1>
-->&nbsp;<!--
--><span id="entryName"></span><!--
-->&nbsp;<!--
--><input id="searchBox" type="search" name="q" value=""><!--
--><input id="reset" type="reset" value="X" alt="Clear the search form"><!--
--><input id="submit" type="submit" value="Search"><!--
--></h1><!--
--></form>
<hr/> <hr/>
<div id="container"></div>
</body> </body>
</html> </html>

View file

@ -20,10 +20,7 @@
selectedIndex = 0, selectedOpts = {}, selectedArray = [], currentIndex = 0, currentOpts = {}, currentArray = [], selectedIndex = 0, selectedOpts = {}, selectedArray = [], currentIndex = 0, currentOpts = {}, currentArray = [],
ajaxLoader = null, imgPreloader = new Image(), ajaxLoader = null, imgPreloader = new Image(), imgRegExp = /\.(jpg|gif|png|bmp|jpeg)(.*)?$/i, swfRegExp = /[^\.]\.(swf)\s*$/i,
imgRegExp = /\.(jpg|gif|png|bmp|jpeg)(.*)?$/i,
movieRegExp = /\.(mov)(.*)?$/i,
swfRegExp = /[^\.]\.(swf)\s*$/i,
loadingTimer, loadingFrame = 1, loadingTimer, loadingFrame = 1,
@ -243,7 +240,7 @@
}, },
_finish = function () { _finish = function () {
inner.css('overflow', (currentOpts.scrolling == 'auto' ? (currentOpts.type == 'image' || currentOpts.type == 'iframe' || currentOpts.type == 'swf' || currentOpts.type == 'movie' ? 'hidden' : 'auto') : (currentOpts.scrolling == 'yes' ? 'auto' : 'visible'))); inner.css('overflow', (currentOpts.scrolling == 'auto' ? (currentOpts.type == 'image' || currentOpts.type == 'iframe' || currentOpts.type == 'swf' ? 'hidden' : 'auto') : (currentOpts.scrolling == 'yes' ? 'auto' : 'visible')));
if (!$.support.opacity) { if (!$.support.opacity) {
inner.get(0).style.removeProperty('filter'); inner.get(0).style.removeProperty('filter');
@ -553,9 +550,6 @@
if (href.match(imgRegExp)) { if (href.match(imgRegExp)) {
type = 'image'; type = 'image';
} else if (href.match(movieRegExp)) {
type = 'movie';
} else if (href.match(swfRegExp)) { } else if (href.match(swfRegExp)) {
type = 'swf'; type = 'swf';
@ -577,7 +571,7 @@
selectedOpts.href = href; selectedOpts.href = href;
selectedOpts.title = title; selectedOpts.title = title;
if (selectedOpts.autoDimensions && selectedOpts.type !== 'iframe' && selectedOpts.type !== 'swf' && selectedOpts.type !== 'movie') { if (selectedOpts.autoDimensions && selectedOpts.type !== 'iframe' && selectedOpts.type !== 'swf') {
selectedOpts.width = 'auto'; selectedOpts.width = 'auto';
selectedOpts.height = 'auto'; selectedOpts.height = 'auto';
} }
@ -642,13 +636,6 @@
break; break;
case 'movie':
str = '<video controls width="' + selectedOpts.width + '" height="' + selectedOpts.height + '" '
+ 'src="' + href + '" poster="' + selectedOpts.poster + '"></video>'
tmp.html(str);
fancybox_process_inline();
break;
case 'swf': case 'swf':
str = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="' + selectedOpts.width + '" height="' + selectedOpts.height + '"><param name="movie" value="' + href + '"></param>'; str = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="' + selectedOpts.width + '" height="' + selectedOpts.height + '"><param name="movie" value="' + href + '"></param>';
emb = ''; emb = '';

View file

@ -1,7 +1,7 @@
$(function() { $(function() {
function formatCaption(title, currentArray, currentIndex, currentOpts) { function formatTitle(title, currentArray, currentIndex, currentOpts) {
var captionElement = document.getElementById(title); var captionElement = document.getElementById(title);
return captionElement.innerHTML; return captionElement.innerHTML;
} }
@ -15,19 +15,8 @@ $(function() {
} }
function encUri(str) {
return encodeURI(str)
.replace(/%5B/g, "[")
.replace(/%5D/g, "]")
.replace(/[!'()*?&#]/g, function (c) {
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
});
}
function renderGrid(data, textStatus) { function renderGrid(data, textStatus) {
var container = $("#container");
container.empty();
$("#name").text(data.name); $("#name").text(data.name);
document.title = data.name; document.title = data.name;
@ -36,96 +25,89 @@ $(function() {
var movieSize = 640; var movieSize = 640;
var prevName = null, thisName; var prevName = null, thisName;
function updateButton(selector, direction, destination) {
var imgName = destination ? direction : direction + "-inactive",
imgUrl = "/photo/assets/" + imgName + ".png",
imgElement = $(selector),
isWrapped = imgElement.parent().get()[0].tagName == "A",
anchor = isWrapped ? imgElement.parent() : $("<a></a>");
if (destination) {
anchor.attr("href", encUri("/photo/album" + destination + ".album") + location.search);
if (!isWrapped) {
imgElement.wrap(anchor);
}
} else {
if (isWrapped) {
imgElement.unwrap();
}
}
imgElement.attr("src", imgUrl);
}
$("#entryName").text(data.name);
$("#arrowLeft").attr(data.name);
updateButton("#arrowLeft", "left", data.prev);
updateButton("#arrowUp", "up", data.parent);
updateButton("#arrowRight", "right", data.next);
if (location.search) {
let params = new URL(location.href).searchParams;
if (params.has("q")) {
$("#searchBox").val(params.get("q"));
}
}
$.each(data.contents, function(idx, entry) { $.each(data.contents, function(idx, entry) {
if (data.groupPrefix && entry.type == "dir") { if (data.groupPrefix && entry.type == "dir") {
thisName = entry.earliest; thisName = entry.earliest;
if (prevName != null && prevName != thisName) { if (prevName != null && prevName != thisName) {
$("<hr/>").appendTo(container); $("<hr/>").appendTo("body");
prevName = null; prevName = null;
} }
if (prevName == null) { if (prevName == null) {
$("<h2></h2>").text(thisName).appendTo(container); $("<h2></h2>").text(thisName).appendTo("body");
prevName = thisName; prevName = thisName;
} }
} }
var dim = scale(entry.width, entry.height, thmb); var dim = scale(entry.width, entry.height, thmb);
var href, poster;
if (entry.thumbtype == "movie") {
href = "/photo/album" + encUri(entry.path) + ".movie";
poster = "/photo/album" + encUri(entry.path) + ".frame";
} else {
href = "/photo/album" + encUri(entry.path) + "?size=" + picSize;
poster = "/photo/album" + encUri(entry.path);
}
var gridDiv = $("<div class=\"grid\">\n" var gridDiv = $("<div class=\"grid\">\n"
+ " <span class=\"name\">" + entry.name + "</span><br/>\n" + " <span class=\"name\">" + entry.name + "</span><br/>\n"
+ " <div class=\"imgborder\"><a class=\"ss\" id=\"ent" + idx + "\" href=\"" + href + "\" title=\"" + entry.name + "\">" + " <div class=\"imgborder\"><a class=\"ss\" id=\"ent" + idx + "\" href=\"/photo/album" + encodeURIComponent(entry.path) + "?size=" + picSize + "\" title=\"" + entry.name + "\">"
+ "<img class=\"" + entry.type + "pic\" src=\"" + poster + "?size=" + thmb + "\" border=\"0\" width=\"" + dim.w + "\" height=\"" + dim.h + "\"/></a></div>\n" + "<img class=\"" + entry.type + "pic\" src=\"/photo/album" + (entry.thumbtype == "movie" ? encodeURIComponent(entry.path) + ".frame" : encodeURIComponent(entry.path)) + "?size=" + thmb + "\" border=\"0\" width=\"" + dim.w + "\" height=\"" + dim.h + "\"/></a></div>\n"
+ "</div>\n"); + "</div>\n");
var captionP = $("<p class=\"caption\"></p>\n"); var captionP = $("<p id=\"" + entry.name + "\" class=\"caption\"></p>\n");
captionP.attr("id", entry.name);
if (entry.caption) { if (entry.caption) {
captionP.text(entry.caption); captionP.text(entry.caption);
} }
captionP.appendTo(gridDiv); captionP.appendTo(gridDiv);
gridDiv.appendTo(container); gridDiv.appendTo('body');
switch (entry.type) { switch (entry.type) {
case "movie": case "movie":
case "image": var size = scale(entry.width, entry.height, movieSize);
var largeDim = scale(entry.width, entry.height, picSize); var href = "/photo/album" + escape(entry.path) + ".movie?size=" + movieSize;
var options = { $("#ent" + idx).attr("rel", "album").attr("href", href).fancybox({
'titlePosition' : 'inside', 'titlePosition' : 'inside',
'transitionIn' : 'elastic', 'transitionIn' : 'elastic',
'transitionOut' : 'elastic', 'transitionOut' : 'elastic',
'easingIn' : 'easeInQuad', 'easingIn' : 'easeInQuad',
'easingOut' : 'easeOutQuad', 'easingOut' : 'easeOutQuad',
'titleFormat' : formatCaption, 'titleFormat' : formatTitle,
'padding' : 0,
'href' : "/photo/assets/flowplayer/flowplayer-3.0.3.swf",
'width' : size.w,
'height' : size.h,
'type' : 'swf',
'swf' : {
'allowfullscreen' : 'true',
'wmode' : 'transparent',
'flashvars':
"config={\
'clip': {\
'url': '" + href + "'\
},\
'plugins': {\
'controls': {\
'url': '/photo/assets/flowplayer/flowplayer.controls-3.0.3.swf',\
'backgroundColor': 'transparent',\
'progressColor': 'transparent',\
'bufferColor': 'transparent',\
'play':true,\
'fullscreen':true,\
'autoHide': 'always'\
}\
}\
}"
}
});
break;
case "image":
var largeDim = scale(entry.width, entry.height, picSize);
$("#ent" + idx).attr("rel", "album").fancybox({
'titlePosition' : 'inside',
'transitionIn' : 'elastic',
'transitionOut' : 'elastic',
'easingIn' : 'easeInQuad',
'easingOut' : 'easeOutQuad',
'titleFormat' : formatTitle,
'width' : largeDim.w, 'width' : largeDim.w,
'height' : largeDim.h, 'height' : largeDim.h,
'type' : entry.type, 'type' : "image",
'href' : href 'href' : $("#ent" + idx).href
}; });
if (entry.thumbtype == "movie") {
options['poster'] = poster + "?size=" + largeDim.w + "w";
}
$("#ent" + idx).attr("rel", "album").fancybox(options);
break; break;
case "dir": case "dir":
$("#ent" + idx).attr("href", location.pathname.replace(/\.album?($|\?)/, "/" + entry.name + ".album")); $("#ent" + idx).attr("href", location.pathname.replace(/\.album?($|\?)/, "/" + entry.name + ".album"));
@ -133,7 +115,18 @@ $(function() {
} }
}); function buildButton(direction, destination, transform) {
var imgName = destination ? direction : direction + "-inactive",
imgElement = $("<img class=\"nav\" width=\"26\" height=\"32\" src=\"/photo/assets/" + imgName + ".png\"/>");
if (destination) {
return $("<a></a>").attr("href", "/photo/album" + destination + ".album").append(imgElement);
} else {
return imgElement;
}
}
$("#titleBar").html("").append(buildButton("left", data.prev)).append(buildButton("up", data.parent)).append(buildButton("right", data.next)).append(" " + data.name);
var selectedImg = window.location.search; var selectedImg = window.location.search;
@ -150,32 +143,15 @@ $(function() {
if (selectedPos !== undefined) { if (selectedPos !== undefined) {
$("#ent" + selectedPos).trigger('click'); $("#ent" + selectedPos).trigger('click');
} }
});
} }
$("#search").on("submit", function () {
var searchStr = "?q=" + encUri($("#searchBox").val()); $("#search").each(function (id, el) {
history.pushState({ }, null, searchStr); el.action = location.pathname.replace(/\.album(\?|$)/, '.search');
$.getJSON(location.pathname.replace(/\.album(\?|$)/, '.json') + searchStr, renderGrid);
return false;
}); });
function resetSearch() {
$("#searchBox").val("");
history.pushState({ }, null, location.pathname);
$.getJSON(location.pathname.replace(/\.album(\?|$)/, '.json'), renderGrid); $.getJSON(location.pathname.replace(/\.album(\?|$)/, '.json'), renderGrid);
return false;
}
$("#search").on("reset", resetSearch);
$("#searchBox").keyup(function (e) {
if (e.key === "Escape") {
resetSearch();
}
});
$.getJSON(location.pathname.replace(/\.album(\?|$)/, '.json') + location.search, renderGrid);
}); });