From 9fac9425cebecf49244bca911b661a2967e49d1e Mon Sep 17 00:00:00 2001 From: Knut Forkalsrud Date: Sat, 4 Nov 2023 13:37:03 -0700 Subject: [PATCH] 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" --- pom.xml | 4 +- .../forkalsrud/album/exif/DirectoryEntry.java | 4 +- .../album/exif/DirectoryEntryFactory.java | 8 +- .../exif/DirectoryMetadataGenerator.java | 12 +- .../forkalsrud/album/exif/SearchEngine.java | 1 - .../forkalsrud/album/exif/SearchResults.java | 3 + .../forkalsrud/album/video/MovieCoder.java | 203 +---- .../album/video/MovieMetadataGenerator.java | 195 ++++ .../forkalsrud/album/web/AlbumServlet.java | 836 ++++++++++-------- .../forkalsrud/album/web/PictureScaler.java | 7 +- src/main/webapp/WEB-INF/ng.html | 2 +- src/main/webapp/WEB-INF/velocity/dynamic.vm | 2 +- src/main/webapp/WEB-INF/velocity/photo.vm | 8 +- src/main/webapp/assets/render.js | 4 + 14 files changed, 703 insertions(+), 586 deletions(-) create mode 100644 src/main/java/org/forkalsrud/album/video/MovieMetadataGenerator.java diff --git a/pom.xml b/pom.xml index bf53127..10ed6f8 100644 --- a/pom.xml +++ b/pom.xml @@ -38,8 +38,8 @@ maven-compiler-plugin 2.3.2 - 1.8 - 1.8 + 8 + 8 diff --git a/src/main/java/org/forkalsrud/album/exif/DirectoryEntry.java b/src/main/java/org/forkalsrud/album/exif/DirectoryEntry.java index 47cd769..1a1ef9b 100644 --- a/src/main/java/org/forkalsrud/album/exif/DirectoryEntry.java +++ b/src/main/java/org/forkalsrud/album/exif/DirectoryEntry.java @@ -36,6 +36,7 @@ public class DirectoryEntry extends EntryWithChildren { boolean childrenLoaded = false; Comparator sort = null; Date earliest = null; + boolean groupByYear; public DirectoryEntry(ServiceApi api, File file) { super(file); @@ -197,6 +198,7 @@ public class DirectoryEntry extends EntryWithChildren { } } this.earliest = oldest; + this.groupByYear = "year".equalsIgnoreCase(props.getProperty("group")); if (thumbnail == null && !children.isEmpty()) { setThumbnail(children.get(0).getThumbnail()); } @@ -212,7 +214,7 @@ public class DirectoryEntry extends EntryWithChildren { @Override public boolean groupByYear() { - return parent == null; + return this.groupByYear; } diff --git a/src/main/java/org/forkalsrud/album/exif/DirectoryEntryFactory.java b/src/main/java/org/forkalsrud/album/exif/DirectoryEntryFactory.java index 3821d25..6c1e862 100644 --- a/src/main/java/org/forkalsrud/album/exif/DirectoryEntryFactory.java +++ b/src/main/java/org/forkalsrud/album/exif/DirectoryEntryFactory.java @@ -1,18 +1,20 @@ package org.forkalsrud.album.exif; import java.io.File; +import java.io.IOException; import org.forkalsrud.album.db.DirectoryDatabase; -import org.forkalsrud.album.video.MovieCoder; +import org.forkalsrud.album.video.MovieMetadataGenerator; public class DirectoryEntryFactory implements DirectoryEntry.ServiceApi { private DirectoryDatabase dirDb; private DirectoryMetadataGenerator generator; + private MovieMetadataGenerator movieGenerator; - public DirectoryEntryFactory(DirectoryDatabase dirDb, MovieCoder movieCoder) { + public DirectoryEntryFactory(DirectoryDatabase dirDb) throws IOException, InterruptedException { this.dirDb = dirDb; - this.generator = new DirectoryMetadataGenerator(movieCoder); + this.generator = new DirectoryMetadataGenerator(new MovieMetadataGenerator()); } diff --git a/src/main/java/org/forkalsrud/album/exif/DirectoryMetadataGenerator.java b/src/main/java/org/forkalsrud/album/exif/DirectoryMetadataGenerator.java index 262fffd..13397ce 100644 --- a/src/main/java/org/forkalsrud/album/exif/DirectoryMetadataGenerator.java +++ b/src/main/java/org/forkalsrud/album/exif/DirectoryMetadataGenerator.java @@ -19,13 +19,11 @@ import javax.imageio.stream.ImageInputStream; import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageProcessingException; -import com.drew.imaging.jpeg.JpegProcessingException; import org.forkalsrud.album.db.DirectoryProps; -import org.forkalsrud.album.video.MovieCoder; +import org.forkalsrud.album.video.MovieMetadataGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.drew.imaging.jpeg.JpegMetadataReader; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.MetadataException; @@ -39,10 +37,10 @@ public class DirectoryMetadataGenerator { final static String CACHE_FILE = "cache.properties"; final static String OVERRIDE_FILE = "album.properties"; - MovieCoder movieCoder; + MovieMetadataGenerator movieMetadataGenerator; - public DirectoryMetadataGenerator(MovieCoder movieCoder) { - this.movieCoder = movieCoder; + public DirectoryMetadataGenerator(MovieMetadataGenerator generator) { + this.movieMetadataGenerator = generator; } @@ -91,7 +89,7 @@ public class DirectoryMetadataGenerator { addPropsForFile(props, f, p); } } else if (name.endsWith(".mov") || name.endsWith(".MOV") || name.endsWith(".mp4") || name.endsWith(".m4v") || name.endsWith(".avi")) { - Map p = movieCoder.generateVideoProperties(f); + Map p = movieMetadataGenerator.generateVideoProperties(f); addPropsForFile(props, f, p); } } diff --git a/src/main/java/org/forkalsrud/album/exif/SearchEngine.java b/src/main/java/org/forkalsrud/album/exif/SearchEngine.java index 93224c5..0c23144 100644 --- a/src/main/java/org/forkalsrud/album/exif/SearchEngine.java +++ b/src/main/java/org/forkalsrud/album/exif/SearchEngine.java @@ -1,7 +1,6 @@ package org.forkalsrud.album.exif; import java.io.File; -import java.nio.file.Path; import java.util.LinkedList; public class SearchEngine { diff --git a/src/main/java/org/forkalsrud/album/exif/SearchResults.java b/src/main/java/org/forkalsrud/album/exif/SearchResults.java index a9d910f..3f5219d 100644 --- a/src/main/java/org/forkalsrud/album/exif/SearchResults.java +++ b/src/main/java/org/forkalsrud/album/exif/SearchResults.java @@ -21,6 +21,9 @@ public class SearchResults extends EntryWithChildren { protected SearchResults(DirectoryEntry root) { super(root); + this.next = root.next; + this.prev = root.prev; + this.parent = root.parent; } public void addMatch(Entry entry) { diff --git a/src/main/java/org/forkalsrud/album/video/MovieCoder.java b/src/main/java/org/forkalsrud/album/video/MovieCoder.java index fe5d0df..890b1b5 100644 --- a/src/main/java/org/forkalsrud/album/video/MovieCoder.java +++ b/src/main/java/org/forkalsrud/album/video/MovieCoder.java @@ -6,15 +6,10 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.OutputStream; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.apache.commons.io.IOUtils; -import com.fasterxml.jackson.databind.ObjectMapper; import org.forkalsrud.album.db.Chunk; import org.forkalsrud.album.db.MovieDatabase; import org.forkalsrud.album.exif.Dimension; @@ -28,14 +23,11 @@ public class MovieCoder { private String ffmpegExecutable; private String mplayerExecutable; - private String exiftoolExecutable; private PictureScaler pictureScaler; - private MovieDatabase movieDb; private HashMap currentEncodings = new HashMap(); - public MovieCoder(PictureScaler pictureScaler, MovieDatabase movieDb) { + public MovieCoder(PictureScaler pictureScaler) { this.pictureScaler = pictureScaler; - this.movieDb = movieDb; } public void init() throws Exception { @@ -45,175 +37,6 @@ public class MovieCoder { this.ffmpegExecutable = util.findExecutableInShellPath("ffmpeg"); } this.mplayerExecutable = util.findExecutableInShellPath("mplayer"); - this.exiftoolExecutable = util.findExecutableInShellPath("exiftool"); - } - - - - - /** - * - * @param f the movie file - * @return a map with the following keys: orientation dimensions captureDate comment etag - * @throws IOException - * @throws InterruptedException - */ - public Map 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 props = new HashMap(); - 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> userDataList = mapper.readValue(p.getInputStream(), List.class); - p.waitFor(); - - props.put("type", "movie"); - - Map userData = userDataList.get(0); - - System.out.println(userData); - { - // The orientation is about flipping and rotating. Here is what an 'F' looks like - // on pictures with each orientation. - // - // 1 2 3 4 5 6 7 8 - // - // 888888 888888 88 88 8888888888 88 88 8888888888 - // 88 88 88 88 88 88 88 88 88 88 88 88 - // 8888 8888 8888 8888 88 8888888888 8888888888 88 - // 88 88 88 88 - // 88 88 888888 888888 - // - // The first four are obtained with only flipping X and/or Y - // The last four are obtained by rotating 90 degrees and then flipping X and/or Y. - // - String orientation; - Object rotationObj = userData.get("Rotation"); - if (rotationObj == null) { - orientation = "1"; - } else { - String rotation = rotationObj.toString(); - if ("0".equals(rotation)) { - orientation = "1"; - } else if ("90".equals(rotation)) { - orientation = "6"; - } else if ("180".equals(rotation)) { - orientation = "3"; - } else if ("270".equals(rotation)) { - orientation = "8"; - } else { - log.warn("unknown rotation: " + rotation + " for file " + f); - orientation = "1"; - } - } - props.put("orientation", orientation); - } - - Object imageSize = userData.get("ImageSize"); - if (imageSize == null) { - imageSize = props.get("ImageWidth") + "x" + props.get("ImageHeight"); - } - props.put("dimensions", imageSize.toString()); - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss"); - props.put("captureDate", sdf.format(new Date(f.lastModified()))); - - props.put("etag", Integer.toHexString(f.getName().hashCode() + Long.valueOf(f.lastModified()).hashCode())); - for (String prop : new String[] { "Duration", "MediaDuration", "PlayDuration" }) { - Object o = userData.get(prop); - if (o != null) { - props.put("length", o.toString().split(" ")[0]); - break; - } - } - return props; } @@ -268,9 +91,9 @@ public class MovieCoder { * @return */ private synchronized EncodingProcess submitEncodingJob(File file, - Thumbnail thumbnail, Dimension targetSize, String key) { + Thumbnail thumbnail, Dimension targetSize, String key, MovieDatabase movieDb) { EncodingProcess ep; - ep = new EncodingProcess(file, thumbnail, targetSize); + ep = new EncodingProcess(file, thumbnail, targetSize, movieDb); currentEncodings.put(key, ep); new Thread(ep).start(); return ep; @@ -295,8 +118,10 @@ public class MovieCoder { private long fileTimestamp; private int orientation; private volatile boolean done = false; + private MovieDatabase movieDb; - public EncodingProcess(File file, Thumbnail thumbnail, Dimension size) { + + public EncodingProcess(File file, Thumbnail thumbnail, Dimension size, MovieDatabase movieDb) { this.file = file; this.fileTimestamp = file.lastModified(); this.targetSize = size.even(); @@ -305,6 +130,7 @@ public class MovieCoder { extraMeta.setDuration(thumbnail.getDuration()); this.filter = new FlvFilter(this, extraMeta); this.orientation = thumbnail.getOrientation(); + this.movieDb = movieDb; } /* @@ -527,10 +353,10 @@ public class MovieCoder { // TODO (knut 05 JUL 2011) Come up with a better interface for supporting range requests etcetera - public void stream(File file, Thumbnail thumbnail, String size, OutputStream out) { + public void stream(File file, Thumbnail thumbnail, String size, OutputStream out, MovieDatabase movieDb) { try { - grabStream(file, thumbnail, size).stream(out); + grabStream(file, thumbnail, size, movieDb).stream(out); } catch (Exception e) { log.error("stream fail", e); } @@ -543,7 +369,7 @@ public class MovieCoder { - synchronized VideoStreamer grabStream(File file, Thumbnail thumbnail, String size) { + synchronized VideoStreamer grabStream(File file, Thumbnail thumbnail, String size, MovieDatabase movieDb) { Dimension targetSize = thumbnail.getSize().scale(size).even(); String key = key(file, targetSize); @@ -563,9 +389,9 @@ public class MovieCoder { } // If neither we need to start the encoding process if (chunk == null && ep == null) { - ep = submitEncodingJob(file, thumbnail, targetSize, key); + ep = submitEncodingJob(file, thumbnail, targetSize, key, movieDb); } - return new VideoStreamer(key, ep, chunk); + return new VideoStreamer(key, ep, chunk, movieDb); } @@ -576,11 +402,14 @@ public class MovieCoder { private Chunk chunk; private String key; private boolean done = false; + private MovieDatabase movieDb; - private VideoStreamer(String key, EncodingProcess ep, Chunk chunk0) { + + private VideoStreamer(String key, EncodingProcess ep, Chunk chunk0, MovieDatabase movieDb) { this.key = key; this.ep = ep; this.chunk = chunk0; + this.movieDb = movieDb; // Range requests can hook in here // if we have chunk metadata in chunk0 we can use that to compute the first // chunk we want and set this.chunkNo accordingly. Otherwise (not likely diff --git a/src/main/java/org/forkalsrud/album/video/MovieMetadataGenerator.java b/src/main/java/org/forkalsrud/album/video/MovieMetadataGenerator.java new file mode 100644 index 0000000..84bf3a4 --- /dev/null +++ b/src/main/java/org/forkalsrud/album/video/MovieMetadataGenerator.java @@ -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 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 props = new HashMap(); + 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> userDataList = mapper.readValue(p.getInputStream(), List.class); + p.waitFor(); + + props.put("type", "movie"); + + Map 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; + } + +} diff --git a/src/main/java/org/forkalsrud/album/web/AlbumServlet.java b/src/main/java/org/forkalsrud/album/web/AlbumServlet.java index dcc0f8f..0ed4b8e 100644 --- a/src/main/java/org/forkalsrud/album/web/AlbumServlet.java +++ b/src/main/java/org/forkalsrud/album/web/AlbumServlet.java @@ -7,13 +7,14 @@ import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.Properties; +import java.util.*; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; @@ -21,6 +22,8 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import org.apache.log4j.PropertyConfigurator; import org.forkalsrud.album.db.DirectoryDatabase; import org.forkalsrud.album.db.DirectoryProps; @@ -39,6 +42,7 @@ import org.springframework.web.util.HtmlUtils; import com.sleepycat.je.Environment; import com.sleepycat.je.EnvironmentConfig; + public class AlbumServlet extends HttpServlet { @@ -93,18 +97,399 @@ public class AlbumServlet "com.sleepycat.je.cleaner.FileProcessor", "com.sleepycat.je.recovery.RecoveryManager"); } - - File base; - String basePrefix; + + + + + /** + * maps files to URIs relative to servlet, e.g. /home/joe/photos/holiday/arrival.jpg -> /photos/holiday/arrival.jpg + * assuming base is /home/joe/photos + */ + public class Mapper { + + private File base; + + public Mapper(File base) { + this.base = base; + } + + public String map(File file) { + StringBuilder buf = new StringBuilder(); + return appendFile(buf, file).toString(); + } + + StringBuilder appendFile(StringBuilder buf, File file) { + if (file == null) { + return buf; + } + if (base.equals(file.getAbsoluteFile())) { + return buf.append("/").append(base.getName()); + } else { + return appendFile(buf, file.getParentFile()).append('/').append(file.getName()); + } + } + + Calendar cal = Calendar.getInstance(); + + public String year(Date d) { + if (d == null) { + return ""; + } + cal.setTime(d); + return String.valueOf(cal.get(Calendar.YEAR)); + } + } + + + class Root { + + String name; + File base; + String basePrefix; + DirectoryEntryFactory dirEntryFactory; + + private Environment environment; + ThumbnailDatabase thumbDb; + DirectoryDatabase dirDb; + MovieDatabase movieDb; + Entry cachedRootNode = null; + + public Root(String name, File base, File dbDir) { + this.name = name; + this.base = base; + this.basePrefix = "/" + base.getName(); + + + EnvironmentConfig environmentConfig = new EnvironmentConfig(); + environmentConfig.setAllowCreate(true); + environmentConfig.setTransactional(true); + this.environment = new Environment(dbDir, environmentConfig); + + this.thumbDb = new ThumbnailDatabase(environment); + this.dirDb = new DirectoryDatabase(environment); + this.movieDb = new MovieDatabase(environment); + + + try { + dirEntryFactory = new DirectoryEntryFactory(dirDb); + } catch (IOException | InterruptedException e) { + log.error("initialization of " + name, e); + } + } + + + public void flushCache() { + cachedRootNode = null; + } + + + public void destroy() { + dirDb.destroy(); + thumbDb.destroy(); + environment.close(); + } + + public String getName() { + return name; + } + + + + + Entry resolveEntry(String pathInfo) { + + if (pathInfo == null || "/".equals(pathInfo)) return resolve(base); + return resolve(new File(base.getParentFile(), pathInfo)); + } + + Entry resolve(File file) { + + if (base.equals(file.getAbsoluteFile())) { + synchronized (this) { + if (cachedRootNode == null) { + cachedRootNode = dirEntryFactory.getEntry(file, null); + } + } + return cachedRootNode; + } else { + return ((DirectoryEntry)resolve(file.getParentFile())).get(file); + } + } + + + + + void handleSearch(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception { + String query = req.getParameter("q"); + + SearchEngine search = new SearchEngine(entry); + SearchResults results = search.search(query); + + res.setContentType("text/html"); + req.setAttribute("search", query); + req.setAttribute("entry", results); + req.setAttribute("thmb", new Integer(250)); + req.setAttribute("full", new Integer(800)); + RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm"); + rd.forward(req, res); + } + + + void handlePhoto(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception { + res.setContentType("text/html"); + req.setAttribute("entry", entry); + req.setAttribute("thmb", new Integer(800)); + RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm"); + rd.forward(req, res); + } + + void handleAlbum(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception { + res.setContentType("text/html"); + req.setAttribute("entry", entry); + req.setAttribute("thmb", new Integer(250)); + req.setAttribute("full", new Integer(800)); + req.setAttribute("D", "$"); + RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/ng.html"); + rd.forward(req, res); + } + + void handleMovieFrame(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception { + + File file = entry.getPath(); + if (notModified(req, file)) { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days + log.info(file.getName() + " not modified (based on date)"); + return; + } + int secondNo = 3; + String size = req.getParameter("size"); + if (size == null) { + size = "250"; + } + String key = file.getPath() + ":" + secondNo + ":" + size; + CachedImage cimg = thumbDb.load(key); + if (cimg != null) { + long fileTs = file.lastModified(); + if (cimg.lastModified >= fileTs) { + log.info("cache hit on " + key); + } else { + log.info(" " + key + " has changed so cache entry wil be refreshed"); + cimg = null; + } + } + if (cimg == null) { + try { + cimg = movieCoder.extractFrame(file, secondNo, entry.getThumbnail(), size); + thumbDb.store(key, cimg); + log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries"); + } catch (Exception e) { + //e.fillInStackTrace(); + throw new RuntimeException("sadness", e); + } + } + try { + res.setStatus(HttpServletResponse.SC_OK); + res.setDateHeader("Last-Modified", file.lastModified()); + res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days + res.setContentType(cimg.mimeType); + res.setContentLength(cimg.bits.length); + res.getOutputStream().write(cimg.bits); + } catch (Exception ex) { + throw new RuntimeException("sadness", ex); + } + } + + void handleMovie(HttpServletRequest req, HttpServletResponse res, FileEntry entry) { + + File file = entry.getPath(); + if (notModified(req, file)) { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days + log.info(file.getName() + " not modified (based on date)"); + return; + } + try { + String size = req.getParameter("size"); + res.setStatus(HttpServletResponse.SC_OK); + res.setDateHeader("Last-Modified", entry.getPath().lastModified()); + res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days + // res.setHeader("Cache-control", "no-cache"); + res.setContentType("video/x-flv"); + movieCoder.stream(entry.getPath(), entry.getThumbnail(), size, res.getOutputStream(), movieDb); + } catch (Exception ex) { + log.error("darn", ex); + throw new RuntimeException("sadness", ex); + } + } + + + void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) { + try { + Mapper mapper = new Mapper(base); + res.setContentType("application/json"); + res.setCharacterEncoding("UTF-8"); + PrintWriter out = res.getWriter(); + out.println("{"); + out.println(" \"name\": " + jsStr(entry.getName()) + ","); + if (entry.parent() != null && "dir".equals(entry.parent().getType())) { + out.println(" \"parent\": " + jsStr(mapper.map(entry.parent().getPath())) + ","); + } + if (entry.prev() != null && "dir".equals(entry.prev().getType())) { + out.println(" \"prev\": " + jsStr(mapper.map(entry.prev().getPath())) + ","); + } + if (entry.next() != null && "dir".equals(entry.next().getType())) { + out.println(" \"next\": " + jsStr(mapper.map(entry.next().getPath())) + ","); + } + if (entry.groupByYear()) { + out.println(" \"groupPrefix\": 4,"); + } + out.println(" \"contents\": ["); + int n = 0; + for (Entry e : entry.getContents()) { + try { + if (n++ > 0) out.println(","); + out.println(" {"); + out.println(" \"name\": " + jsStr(e.getName()) + ","); + out.println(" \"type\": " + jsStr(e.getType()) + ","); + if ("dir".equals(e.getType())) { + DirectoryEntry de = (DirectoryEntry)e; + if (de.getEarliest() != null) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy"); + out.println(" \"earliest\": " + jsStr(sdf.format(de.getEarliest())) + ","); + } + } + Thumbnail thumb = e.getThumbnail(); + if (thumb != null) { + out.println(" \"path\": " + jsStr(mapper.map(thumb.getPath())) + ","); + out.println(" \"thumbtype\": " + jsStr(thumb.getType()) + ","); + out.println(" \"width\": " + thumb.getSize().getWidth() + ","); + out.print(" \"height\": " + thumb.getSize().getHeight()); + } + if (e.getCaption() != null) { + out.println(",\n \"caption\": " + jsStr(e.getCaption())); + } else { + out.println(); + } + out.print(" }"); + } catch (Exception ex) { + throw new Exception(e.toString(), ex); + } + } + out.println(); + out.println(" ]"); + out.println("}"); + } catch (Exception e) { + throw new RuntimeException("sadness", e); + } + } + + String jsStr(String in) { + return in == null ? "null" : "\"" + in.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\""; + } + + void handleCache(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) { + if (entry == null) { + res.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + DirectoryProps props = entry.getCache(); + if (props == null) { + props = new DirectoryProps(); + } + try { + res.setContentType("text/plain"); + res.setCharacterEncoding("UTF-8"); + PrintWriter out = res.getWriter(); + out.println("# cache timestamp: " + props.getTimestamp()); + out.println("# dir timestamp: " + entry.getPath().lastModified()); + out.println(); + props.store(out, ""); + } catch (Exception e) { + throw new RuntimeException("sadness", e); + } + } + void handleEdit(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception { + String value = req.getParameter("value"); + if (value != null) { + File propertyFile = new File(entry.getPath().getParent(), "album.properties"); + Properties props = new Properties(); + if (propertyFile.exists()) { + FileInputStream fis = new FileInputStream(propertyFile); + props.load(fis); + fis.close(); + } + props.setProperty("file." + entry.getName() + ".caption", value); + FileOutputStream fos = new FileOutputStream(propertyFile); + props.store(fos, "online editor"); + fos.close(); + res.setContentType("text/html"); + res.getWriter().println(HtmlUtils.htmlEscape(value)); + cachedRootNode = null; + return; + } + res.setContentType("text/html"); + req.setAttribute("entry", entry); + req.setAttribute("thmb", new Integer(640)); + RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/edit.vm"); + rd.forward(req, res); + } + + + + + + void procesScaledImageRequest(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException { + + if (notModified(req, file)) { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days + log.info(file.getName() + " not modified (based on date)"); + return; + } + String fileEtag = thumbnail.getEtag() + "-" + size; + if (etagMatches(req, fileEtag)) { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days + log.info(file.getName() + " not modified (based on etag)"); + return; + } + + String key = file.getPath() + ":" + size; + + CachedImage cimg = thumbDb.load(key); + if (cimg != null) { + if (cimg.lastModified == file.lastModified()) { + log.info("cache hit on " + key); + } else { + log.info(" " + key + " has changed so cache entry wil be refreshed"); + cimg = null; + } + } + if (cimg == null) { + cimg = pictureScaler.scalePicture(file, thumbnail, size); + thumbDb.store(key, cimg); + log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries"); + } + res.setStatus(HttpServletResponse.SC_OK); + res.setDateHeader("Last-Modified", file.lastModified()); + res.setHeader("ETag", fileEtag); + res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days + res.setContentType(cimg.mimeType); + res.setContentLength(cimg.bits.length); + res.getOutputStream().write(cimg.bits); + } + } + + + + Map roots = new HashMap(); + + PictureScaler pictureScaler; - long lastCacheFlushTime; - private Environment environment; - ThumbnailDatabase thumbDb; - DirectoryDatabase dirDb; - MovieDatabase movieDb; - Entry cachedRootNode = null; MovieCoder movieCoder; - DirectoryEntryFactory dirEntryFactory; + + long lastCacheFlushTime; long nextCacheRefresh; @Override @@ -131,36 +516,30 @@ public class AlbumServlet long minute = 60 * 1000L; nextCacheRefresh = System.currentTimeMillis() + minute; - base = new File(props.getProperty("base", "photos")).getAbsoluteFile(); - basePrefix = "/" + base.getName(); - - String dbDirName = props.getProperty("dbdir"); - File dbDir; - if (dbDirName != null) { - dbDir = new File(dbDirName); + Set rootNames = props.stringPropertyNames().stream() + .filter(s -> s.startsWith("root.")) + .map(s -> s.split("\\.")[1]) + .collect(Collectors.toSet()); + if (rootNames.isEmpty()) { + File photos = new File("photos"); + File dbdir = new File(System.getProperty("java.io.tmpdir"), "album"); + photos.mkdirs(); + dbdir.mkdirs(); + roots.put("photos", new Root("photos", photos, dbdir)); } else { - dbDir = new File(System.getProperty("java.io.tmpdir"), "album"); - if (dbDir.isDirectory() && dbDir.canWrite()) { - for (File f : dbDir.listFiles()) { - f.delete(); - } - } + rootNames.forEach(name -> { + File base = new File(props.getProperty("root." + name + ".base", "photos")).getAbsoluteFile(); + String dbDirName = props.getProperty("root." + name + ".dbdir"); + File dbDir = new File(dbDirName); + dbDir.mkdirs(); + roots.put(name, new Root(name, base, dbDir)); + }); } - dbDir.mkdirs(); - EnvironmentConfig environmentConfig = new EnvironmentConfig(); - environmentConfig.setAllowCreate(true); - environmentConfig.setTransactional(true); - environment = new Environment(dbDir, environmentConfig); - - thumbDb = new ThumbnailDatabase(environment); - dirDb = new DirectoryDatabase(environment); - movieDb = new MovieDatabase(environment); pictureScaler = new PictureScaler(); - - movieCoder = new MovieCoder(pictureScaler, movieDb); + movieCoder = new MovieCoder(pictureScaler); movieCoder.setFfmpegPath(props.getProperty("ffmpeg.path")); try { movieCoder.init(); @@ -168,7 +547,6 @@ public class AlbumServlet throw new ServletException("unable to locate movie helpers (mplayer and ffmpeg)", e); } - dirEntryFactory = new DirectoryEntryFactory(dirDb, movieCoder); lastCacheFlushTime = System.currentTimeMillis(); } @@ -186,11 +564,25 @@ public class AlbumServlet @Override public void destroy() { log.info("Shutting down Album"); - dirDb.destroy(); - thumbDb.destroy(); - environment.close(); } + + static Pattern ROOT_MATCHER = Pattern.compile("^/(\\w+)"); + + Root rootFor(String pathInfo) { + + if (pathInfo == null) { + return null; + } + Matcher m = ROOT_MATCHER.matcher(pathInfo); + if (!m.find()) { + return null; + } + return roots.get(m.group(1)); + } + + + @Override public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException @@ -206,12 +598,12 @@ public class AlbumServlet req.setAttribute("assets", req.getContextPath() + "/assets"); req.setAttribute("req", req); req.setAttribute("base", req.getContextPath() + req.getServletPath()); - req.setAttribute("mapper", new Mapper()); String pathInfo = req.getPathInfo(); // help the user get to the top level page if (pathInfo == null || "/".equals(pathInfo)) { - String u = req.getContextPath() + "/album/" + base.getName() + ".album"; + Root root = roots.values().stream().findFirst().orElse(null); + String u = req.getContextPath() + "/album/" + root.getName() + ".album"; res.sendRedirect(u); return; } @@ -219,77 +611,83 @@ public class AlbumServlet if ("/_roots.json".equals(pathInfo)) { res.setContentType("application/json"); res.setCharacterEncoding("UTF-8"); - PrintWriter out = res.getWriter(); - out.append("[\"").append(base.getName()).append("\"]"); + + ObjectMapper json = new ObjectMapper(); + ArrayNode arr = json.createArrayNode(); + roots.values().stream().map(r -> r.getName()).forEach(arr::add); + json.writeValue(res.getOutputStream(), arr); return; } long now = System.currentTimeMillis(); if (now > nextCacheRefresh) { - cachedRootNode = null; + roots.values().forEach(Root::flushCache); long minute = 60 * 1000L; nextCacheRefresh = now + minute; } try { - if (pathInfo.startsWith(basePrefix)) { - pathInfo = pathInfo.substring(basePrefix.length()); - } else if (pathInfo.equals("/search")) { - handleSearch(req, res, (DirectoryEntry)resolveEntry("/")); + Root root = rootFor(pathInfo); + if (root == null) { + res.setStatus(HttpServletResponse.SC_NOT_FOUND); return; - } else { - res.sendError(HttpServletResponse.SC_NOT_FOUND, "pathinfo=" + pathInfo); + } + req.setAttribute("mapper", new Mapper(root.base)); + + if (pathInfo.endsWith(".search")) { + pathInfo = pathInfo.substring(0, pathInfo.length() - ".search".length()); + root.handleSearch(req, res, (DirectoryEntry)root.resolveEntry(pathInfo)); return; } if (pathInfo.endsWith(".album")) { pathInfo = pathInfo.substring(0, pathInfo.length() - ".album".length()); - handleAlbum(req, res, (DirectoryEntry)resolveEntry(pathInfo)); + root.handleAlbum(req, res, (DirectoryEntry)root.resolveEntry(pathInfo)); return; } if (pathInfo.endsWith(".photo")) { pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length()); - handlePhoto(req, res, (FileEntry)resolveEntry(pathInfo)); + root.handlePhoto(req, res, (FileEntry)root.resolveEntry(pathInfo)); return; } if (pathInfo.endsWith(".frame")) { pathInfo = pathInfo.substring(0, pathInfo.length() - ".frame".length()); - handleMovieFrame(req, res, (FileEntry)resolveEntry(pathInfo)); + root.handleMovieFrame(req, res, (FileEntry)root.resolveEntry(pathInfo)); return; } if (pathInfo.endsWith(".movie")) { pathInfo = pathInfo.substring(0, pathInfo.length() - ".movie".length()); - handleMovie(req, res, (FileEntry)resolveEntry(pathInfo)); + root.handleMovie(req, res, (FileEntry)root.resolveEntry(pathInfo)); return; } if (pathInfo.endsWith(".edit")) { pathInfo = pathInfo.substring(0, pathInfo.length() - ".edit".length()); - handleEdit(req, res, (FileEntry)resolveEntry(pathInfo)); + root.handleEdit(req, res, (FileEntry)root.resolveEntry(pathInfo)); return; } if (pathInfo.endsWith(".json")) { pathInfo = pathInfo.substring(0, pathInfo.length() - ".json".length()); - DirectoryEntry directoryEntry = (DirectoryEntry)resolveEntry(pathInfo); + DirectoryEntry directoryEntry = (DirectoryEntry)root.resolveEntry(pathInfo); if (directoryEntry == null) { res.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } - handleJson(req, res, directoryEntry); + root.handleJson(req, res, directoryEntry); return; } if (pathInfo.endsWith(".cache")) { pathInfo = pathInfo.substring(0, pathInfo.length() - ".cache".length()); - handleCache(req, res, (DirectoryEntry)resolveEntry(pathInfo)); + root.handleCache(req, res, (DirectoryEntry)root.resolveEntry(pathInfo)); return; } - File file = new File(base, pathInfo); + File file = new File(root.base.getParentFile(), pathInfo); if (!file.canRead()) { res.setStatus(HttpServletResponse.SC_FORBIDDEN); return; @@ -298,8 +696,8 @@ public class AlbumServlet String size = req.getParameter("size"); if (size != null) { - FileEntry e = (FileEntry)resolve(file); - procesScaledImageRequest(req, res, file, e.getThumbnail(), size); + FileEntry e = (FileEntry)root.resolve(file); + root.procesScaledImageRequest(req, res, file, e.getThumbnail(), size); return; } } catch (Exception e) { @@ -308,222 +706,6 @@ public class AlbumServlet res.setStatus(HttpServletResponse.SC_NOT_FOUND); } - void handlePhoto(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception { - res.setContentType("text/html"); - req.setAttribute("entry", entry); - req.setAttribute("thmb", new Integer(800)); - RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm"); - rd.forward(req, res); - } - - void handleAlbum(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception { - res.setContentType("text/html"); - req.setAttribute("entry", entry); - req.setAttribute("thmb", new Integer(250)); - req.setAttribute("full", new Integer(800)); - req.setAttribute("D", "$"); - RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/ng.html"); - rd.forward(req, res); - } - - void handleMovieFrame(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception { - - File file = entry.getPath(); - if (notModified(req, file)) { - res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days - log.info(file.getName() + " not modified (based on date)"); - return; - } - int secondNo = 3; - String size = req.getParameter("size"); - if (size == null) { - size = "250"; - } - String key = file.getPath() + ":" + secondNo + ":" + size; - CachedImage cimg = thumbDb.load(key); - if (cimg != null) { - if (cimg.lastModified == file.lastModified()) { - log.info("cache hit on " + key); - } else { - log.info(" " + key + " has changed so cache entry wil be refreshed"); - cimg = null; - } - } - if (cimg == null) { - try { - cimg = movieCoder.extractFrame(file, secondNo, entry.getThumbnail(), size); - thumbDb.store(key, cimg); - log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries"); - } catch (Exception e) { - //e.fillInStackTrace(); - throw new RuntimeException("sadness", e); - } - } - try { - res.setStatus(HttpServletResponse.SC_OK); - res.setDateHeader("Last-Modified", file.lastModified()); - res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days - res.setContentType(cimg.mimeType); - res.setContentLength(cimg.bits.length); - res.getOutputStream().write(cimg.bits); - } catch (Exception ex) { - throw new RuntimeException("sadness", ex); - } - } - - void handleMovie(HttpServletRequest req, HttpServletResponse res, FileEntry entry) { - - File file = entry.getPath(); - if (notModified(req, file)) { - res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days - log.info(file.getName() + " not modified (based on date)"); - return; - } - try { - String size = req.getParameter("size"); - res.setStatus(HttpServletResponse.SC_OK); - res.setDateHeader("Last-Modified", entry.getPath().lastModified()); - res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days - // res.setHeader("Cache-control", "no-cache"); - res.setContentType("video/x-flv"); - movieCoder.stream(entry.getPath(), entry.getThumbnail(), size, res.getOutputStream()); - } catch (Exception ex) { - log.error("darn", ex); - throw new RuntimeException("sadness", ex); - } - } - - - void handleJson(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) { - try { - Mapper mapper = new Mapper(); - res.setContentType("application/json"); - res.setCharacterEncoding("UTF-8"); - PrintWriter out = res.getWriter(); - out.println("{"); - out.println(" \"name\": " + jsStr(entry.getName()) + ","); - if (entry.parent() != null && "dir".equals(entry.parent().getType())) { - out.println(" \"parent\": " + jsStr(mapper.map(entry.parent().getPath())) + ","); - } - if (entry.prev() != null && "dir".equals(entry.prev().getType())) { - out.println(" \"prev\": " + jsStr(mapper.map(entry.prev().getPath())) + ","); - } - if (entry.next() != null && "dir".equals(entry.next().getType())) { - out.println(" \"next\": " + jsStr(mapper.map(entry.next().getPath())) + ","); - } - if (entry.groupByYear()) { - out.println(" \"groupPrefix\": 4,"); - } - out.println(" \"contents\": ["); - int n = 0; - for (Entry e : entry.getContents()) { - try { - if (n++ > 0) out.println(","); - out.println(" {"); - out.println(" \"name\": " + jsStr(e.getName()) + ","); - out.println(" \"type\": " + jsStr(e.getType()) + ","); - if ("dir".equals(e.getType())) { - DirectoryEntry de = (DirectoryEntry)e; - if (de.getEarliest() != null) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy"); - out.println(" \"earliest\": " + jsStr(sdf.format(de.getEarliest())) + ","); - } - } - Thumbnail thumb = e.getThumbnail(); - if (thumb != null) { - out.println(" \"path\": " + jsStr(mapper.map(thumb.getPath())) + ","); - out.println(" \"thumbtype\": " + jsStr(thumb.getType()) + ","); - out.println(" \"width\": " + thumb.getSize().getWidth() + ","); - out.print(" \"height\": " + thumb.getSize().getHeight()); - } - if (e.getCaption() != null) { - out.println(",\n \"caption\": " + jsStr(e.getCaption())); - } else { - out.println(); - } - out.print(" }"); - } catch (Exception ex) { - throw new Exception(e.toString(), ex); - } - } - out.println(); - out.println(" ]"); - out.println("}"); - } catch (Exception e) { - throw new RuntimeException("sadness", e); - } - } - - String jsStr(String in) { - return in == null ? "null" : "\"" + in.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\""; - } - - void handleCache(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) { - if (entry == null) { - res.setStatus(HttpServletResponse.SC_NOT_FOUND); - return; - } - DirectoryProps props = entry.getCache(); - if (props == null) { - props = new DirectoryProps(); - } - try { - res.setContentType("text/plain"); - res.setCharacterEncoding("UTF-8"); - PrintWriter out = res.getWriter(); - out.println("# cache timestamp: " + props.getTimestamp()); - out.println("# dir timestamp: " + entry.getPath().lastModified()); - out.println(); - props.store(out, ""); - } catch (Exception e) { - throw new RuntimeException("sadness", e); - } - } - void handleEdit(HttpServletRequest req, HttpServletResponse res, FileEntry entry) throws Exception { - String value = req.getParameter("value"); - if (value != null) { - File propertyFile = new File(entry.getPath().getParent(), "album.properties"); - Properties props = new Properties(); - if (propertyFile.exists()) { - FileInputStream fis = new FileInputStream(propertyFile); - props.load(fis); - fis.close(); - } - props.setProperty("file." + entry.getName() + ".caption", value); - FileOutputStream fos = new FileOutputStream(propertyFile); - props.store(fos, "online editor"); - fos.close(); - res.setContentType("text/html"); - res.getWriter().println(HtmlUtils.htmlEscape(value)); - cachedRootNode = null; - return; - } - res.setContentType("text/html"); - req.setAttribute("entry", entry); - req.setAttribute("thmb", new Integer(640)); - RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/edit.vm"); - rd.forward(req, res); - } - - - void handleSearch(HttpServletRequest req, HttpServletResponse res, DirectoryEntry entry) throws Exception { - String query = req.getParameter("q"); - - SearchEngine search = new SearchEngine(entry); - SearchResults results = search.search(query); - - res.setContentType("text/html"); - req.setAttribute("search", query); - req.setAttribute("entry", results); - req.setAttribute("thmb", new Integer(250)); - req.setAttribute("full", new Integer(800)); - RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/velocity/photo.vm"); - rd.forward(req, res); - } - - boolean etagMatches(HttpServletRequest req, String fileEtag) { @@ -550,111 +732,15 @@ public class AlbumServlet return reqDate > 0 && fDate > 0 && fDate <= reqDate; } - void procesScaledImageRequest(HttpServletRequest req, HttpServletResponse res, File file, Thumbnail thumbnail, String size) throws IOException { - - if (notModified(req, file)) { - res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days - log.info(file.getName() + " not modified (based on date)"); - return; - } - String fileEtag = thumbnail.getEtag() + "-" + size; - if (etagMatches(req, fileEtag)) { - res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days - log.info(file.getName() + " not modified (based on etag)"); - return; - } - - String key = file.getPath() + ":" + size; - - CachedImage cimg = thumbDb.load(key); - if (cimg != null) { - if (cimg.lastModified == file.lastModified()) { - log.info("cache hit on " + key); - } else { - log.info(" " + key + " has changed so cache entry wil be refreshed"); - cimg = null; - } - } - if (cimg == null) { - cimg = pictureScaler.scalePicture(file, thumbnail, size); - thumbDb.store(key, cimg); - log.info(" " + key + " added to the cache with size " + cimg.bits.length + " -- now " + thumbDb.size() + " entries"); - } - res.setStatus(HttpServletResponse.SC_OK); - res.setDateHeader("Last-Modified", file.lastModified()); - res.setHeader("ETag", fileEtag); - res.setDateHeader("Expires", System.currentTimeMillis() + (30 * 24 * 3600 * 1000L)); // 30 days - res.setContentType(cimg.mimeType); - res.setContentLength(cimg.bits.length); - res.getOutputStream().write(cimg.bits); - } - Entry resolveEntry(String pathInfo) { - - if (pathInfo == null || "/".equals(pathInfo)) return resolve(base); - return resolve(new File(base, pathInfo)); - } - - Entry resolve(File file) { - - if (base.equals(file.getAbsoluteFile())) { - synchronized (this) { - if (cachedRootNode == null) { - cachedRootNode = dirEntryFactory.getEntry(file, null); - } - } - return cachedRootNode; - } else { - return ((DirectoryEntry)resolve(file.getParentFile())).get(file); - } - } - - - @Override public String getServletInfo() { return "Display of org.forkalsrud.album"; } - - /** - * maps files to URIs relative to servlet, e.g. /home/joe/photos/holiday/arrival.jpg -> /photos/holiday/arrival.jpg - * assuming base is /home/joe/photos - */ - public class Mapper { - - public String map(File file) { - StringBuilder buf = new StringBuilder(); - return appendFile(buf, file).toString(); - } - - StringBuilder appendFile(StringBuilder buf, File file) { - if (file == null) { - return buf; - } - if (base.equals(file.getAbsoluteFile())) { - return buf.append("/").append(base.getName()); - } else { - return appendFile(buf, file.getParentFile()).append('/').append(file.getName()); - } - } - - Calendar cal = Calendar.getInstance(); - - public String year(Date d) { - if (d == null) { - return ""; - } - cal.setTime(d); - return String.valueOf(cal.get(Calendar.YEAR)); - } - } - } // eof diff --git a/src/main/java/org/forkalsrud/album/web/PictureScaler.java b/src/main/java/org/forkalsrud/album/web/PictureScaler.java index 09e4dbe..48f7c7e 100644 --- a/src/main/java/org/forkalsrud/album/web/PictureScaler.java +++ b/src/main/java/org/forkalsrud/album/web/PictureScaler.java @@ -21,11 +21,8 @@ import java.util.concurrent.TimeUnit; import javax.imageio.IIOImage; import javax.imageio.ImageIO; -import javax.imageio.ImageReadParam; -import javax.imageio.ImageReader; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; -import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import org.forkalsrud.album.exif.Dimension; @@ -91,7 +88,7 @@ public class PictureScaler { try { return scalePictureReally(file, thumbnail, size); } catch (Exception e) { - log.error("sadness", e); + log.error("sadness: " + file.getAbsolutePath(), e); return new CachedImage(); } } @@ -208,7 +205,7 @@ public class PictureScaler { xform.scale(flipX[idx], flipY[idx]); xform.translate(-img.getWidth() / 2d, -img.getHeight() / 2d); - int imgType = img.getType(); + int imgType = img.getType() > 0 ? img.getType() : BufferedImage.TYPE_INT_RGB; BufferedImage buf2 = new BufferedImage(intermediate.getWidth(), intermediate.getHeight(), imgType); Graphics2D g2 = buf2.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); diff --git a/src/main/webapp/WEB-INF/ng.html b/src/main/webapp/WEB-INF/ng.html index ade1e31..1ef77d4 100644 --- a/src/main/webapp/WEB-INF/ng.html +++ b/src/main/webapp/WEB-INF/ng.html @@ -84,7 +84,7 @@ -
+

 


diff --git a/src/main/webapp/WEB-INF/velocity/dynamic.vm b/src/main/webapp/WEB-INF/velocity/dynamic.vm index d5bb686..f0f04e2 100644 --- a/src/main/webapp/WEB-INF/velocity/dynamic.vm +++ b/src/main/webapp/WEB-INF/velocity/dynamic.vm @@ -205,7 +205,7 @@ $D(function() { -
+ #macro(navlink $entry)${base}$mapper.map(${entry.getPath()}).#if($entry.isFile())photo#{else}album#end#end #macro(navbutton $entry $direction)#if($entry)#else#end#end

#navbutton(${entry.prev()} "left")#navbutton(${entry.parent()} "up")#navbutton(${entry.next()} "right") $entry.name

diff --git a/src/main/webapp/WEB-INF/velocity/photo.vm b/src/main/webapp/WEB-INF/velocity/photo.vm index 5c96523..031a4c5 100644 --- a/src/main/webapp/WEB-INF/velocity/photo.vm +++ b/src/main/webapp/WEB-INF/velocity/photo.vm @@ -77,8 +77,9 @@ $(document).ready(function() { function formatTitle(title, currentArray, currentIndex, currentOpts) { - var captionElement = document.getElementById(title); - return captionElement.innerHTML; + return $(currentArray[currentIndex]).parent().parent().children("p").html(); + // var captionElement = document.getElementById(title); + // return captionElement.innerHTML; } var selectedImg = window.location.search; var selectedPos = undefined; @@ -91,6 +92,7 @@ $(document).ready(function() { }); } var gallery = $("a.ss").fancybox({ + 'type' : 'image', 'titlePosition' : 'inside', 'transitionIn' : 'elastic', 'transitionOut' : 'elastic', @@ -128,7 +130,7 @@ $(document).ready(function() { #else
#end -

$!en.caption

+

$!en.caption

#end diff --git a/src/main/webapp/assets/render.js b/src/main/webapp/assets/render.js index c69bb61..000f451 100644 --- a/src/main/webapp/assets/render.js +++ b/src/main/webapp/assets/render.js @@ -14,6 +14,10 @@ $(function() { } } + $("#search").each(function (id, el) { + el.action = location.pathname.replace(/\.album(\?|$)/, '.search'); + }); + $.getJSON(location.pathname.replace(/\.album(\?|$)/, '.json'), function(data, textStatus) { $("#name").text(data.name);