diff --git a/source/net/filebot/format/ExpressionFormatMethods.java b/source/net/filebot/format/ExpressionFormatMethods.java index 1f31f57e..3c034365 100644 --- a/source/net/filebot/format/ExpressionFormatMethods.java +++ b/source/net/filebot/format/ExpressionFormatMethods.java @@ -10,6 +10,7 @@ import static net.filebot.media.MediaDetection.*; import static net.filebot.util.RegularExpressions.*; import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; @@ -496,17 +497,13 @@ public class ExpressionFormatMethods { return 0; } - public static long getCreationDate(File self) { - try { - BasicFileAttributes attr = Files.getFileAttributeView(self.toPath(), BasicFileAttributeView.class).readAttributes(); - long creationDate = attr.creationTime().toMillis(); - if (creationDate > 0) { - return creationDate; - } - return attr.lastModifiedTime().toMillis(); - } catch (Exception e) { - throw new RuntimeException(e); + public static long getCreationDate(File self) throws IOException { + BasicFileAttributes attr = Files.getFileAttributeView(self.toPath(), BasicFileAttributeView.class).readAttributes(); + long creationDate = attr.creationTime().toMillis(); + if (creationDate > 0) { + return creationDate; } + return attr.lastModifiedTime().toMillis(); } public static File toFile(String self) { diff --git a/source/net/filebot/format/MediaBindingBean.java b/source/net/filebot/format/MediaBindingBean.java index 0bb9129b..51542445 100644 --- a/source/net/filebot/format/MediaBindingBean.java +++ b/source/net/filebot/format/MediaBindingBean.java @@ -207,15 +207,14 @@ public class MediaBindingBean { if (infoObject instanceof File) { File f = (File) infoObject; - ZonedDateTime d = ImageMetadata.getDateTaken(f); - if (d != null) { - return new SimpleDate(d); - } - - long t = getCreationDate(f); - if (t > 0) { - return new SimpleDate(t); - } + return new ImageMetadata(f).getDateTaken().map(SimpleDate::new).orElseGet(() -> { + try { + return new SimpleDate(getCreationDate(f)); + } catch (Exception e) { + debug.warning(e::toString); + } + return null; + }); } return null; @@ -842,7 +841,12 @@ public class MediaBindingBean { @Define("exif") public AssociativeScriptObject getImageMetadata() throws Exception { - return new AssociativeScriptObject(new ImageMetadata(getMediaFile())); + return new AssociativeScriptObject(new ImageMetadata(getMediaFile()).snapshot(t -> t.getTagName())); + } + + @Define("location") + public List getLocation() throws Exception { + return new ImageMetadata(getMediaFile()).getLocationTaken().orElse(null); } @Define("artist") diff --git a/source/net/filebot/mediainfo/ImageMetadata.java b/source/net/filebot/mediainfo/ImageMetadata.java index c973615d..861ba932 100644 --- a/source/net/filebot/mediainfo/ImageMetadata.java +++ b/source/net/filebot/mediainfo/ImageMetadata.java @@ -1,63 +1,126 @@ package net.filebot.mediainfo; +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static java.util.stream.Collectors.*; import static net.filebot.Logging.*; -import static net.filebot.similarity.Normalization.*; +import static net.filebot.util.JsonUtilities.*; import java.io.File; import java.io.FileFilter; import java.io.IOException; +import java.net.URL; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.Date; +import java.util.EnumMap; import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageProcessingException; +import com.drew.lang.GeoLocation; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.Tag; import com.drew.metadata.exif.ExifIFD0Directory; import com.drew.metadata.exif.ExifSubIFDDirectory; +import com.drew.metadata.exif.GpsDirectory; +import net.filebot.Cache; +import net.filebot.CacheType; import net.filebot.util.FileUtilities.ExtensionFileFilter; -public class ImageMetadata extends LinkedHashMap { +public class ImageMetadata { - public ImageMetadata(File file) throws ImageProcessingException, IOException { - Metadata metadata = ImageMetadataReader.readMetadata(file); + private File file; + private Metadata metadata; - for (Directory directory : metadata.getDirectories()) { - for (Tag tag : directory.getTags()) { - String v = tag.getDescription(); - if (v != null && v.length() > 0) { - putIfAbsent(normalizeSpace(normalizePunctuation(tag.getTagName()), "_"), v); - } - } + public ImageMetadata(File file) { + this.file = file; + } - if (directory.hasErrors()) { - for (String error : directory.getErrors()) { - debug.warning(error); + protected synchronized Metadata getMetadata() throws ImageProcessingException, IOException { + if (metadata == null) { + metadata = ImageMetadataReader.readMetadata(file); + } + return metadata; + } + + protected boolean accept(Directory directory) { + return !directory.getName().matches("JPEG|JFIF|Interoperability|Huffman|File"); + } + + public Map snapshot(Function key) throws ImageProcessingException, IOException { + Map values = new LinkedHashMap(); + + for (Directory directory : getMetadata().getDirectories()) { + if (accept(directory)) { + for (Tag tag : directory.getTags()) { + String v = tag.getDescription(); + if (v != null && v.length() > 0) { + values.put(key.apply(tag), v); + } } } } + + return values; } - public static ZonedDateTime getDateTaken(File file) { + public Optional getDateTaken() { + return extract(m -> m.getFirstDirectoryOfType(ExifIFD0Directory.class).getDate(ExifSubIFDDirectory.TAG_DATETIME)).map(d -> { + return d.toInstant().atZone(ZoneOffset.UTC); + }); + } + + public Optional> getLocationTaken() { + return extract(m -> m.getFirstDirectoryOfType(GpsDirectory.class).getGeoLocation()).map(this::locate); + } + + protected List locate(GeoLocation location) { + try { + // e.g. https://maps.googleapis.com/maps/api/geocode/json?latlng=40.7470444,-073.9411611 + Cache cache = Cache.getCache("geocode", CacheType.Monthly); + Object json = cache.json(location.getLatitude() + "," + location.getLongitude(), pos -> new URL("https://maps.googleapis.com/maps/api/geocode/json?latlng=" + pos)).get(); + + Map address = new EnumMap(AddressComponents.class); + + streamJsonObjects(json, "results").limit(1).forEach(r -> { + streamJsonObjects(r, "address_components").forEach(a -> { + String name = getString(a, "long_name"); + for (Object type : getArray(a, "types")) { + stream(AddressComponents.values()).filter(c -> c.name().equals(type)).findFirst().ifPresent(c -> { + address.putIfAbsent(c, name); + }); + } + }); + }); + + return address.values().stream().filter(Objects::nonNull).collect(toList()); // enum set is always in natural order + } catch (Exception e) { + debug.warning(e::toString); + } + + return emptyList(); + } + + private enum AddressComponents { + country, administrative_area_level_1, administrative_area_level_2, administrative_area_level_3, administrative_area_level_4, sublocality, neighborhood; + } + + protected Optional extract(Function extract) { if (SUPPORTED_FILE_TYPES.accept(file)) { try { - Metadata metadata = ImageMetadataReader.readMetadata(file); - ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); - Date date = directory.getDate(ExifSubIFDDirectory.TAG_DATETIME); - - if (date != null) { - return date.toInstant().atZone(ZoneOffset.UTC); - } + return Optional.ofNullable(extract.apply(getMetadata())); } catch (Exception e) { debug.warning(e::toString); } } - - return null; + return Optional.empty(); } public static final FileFilter SUPPORTED_FILE_TYPES = new ExtensionFileFilter("jpg", "jpeg", "png", "webp", "gif", "ico", "bmp", "tiff", "psd", "pcx", "raw", "crw", "cr2", "nef", "orf", "raf", "rw2", "rwl", "srw", "arw", "dng", "x3f", "mov", "mp4", "m4v", "3g2", "3gp", "3gp"); diff --git a/source/net/filebot/ui/rename/BindingDialog.properties b/source/net/filebot/ui/rename/BindingDialog.properties index 29fa89ce..6e2f5f0d 100644 --- a/source/net/filebot/ui/rename/BindingDialog.properties +++ b/source/net/filebot/ui/rename/BindingDialog.properties @@ -2,4 +2,4 @@ parameter.exclude: ^StreamKind|^UniqueID|^StreamOrder|^ID|Count$ # preview expressions (keys are tagged so they can be sorted alphabetically) -expressions: n, y, s, e, sxe, s00e00, t, d, startdate, absolute, es, e00, sy, sc, di, dc, age, special, episode, series, primaryTitle, alias, movie, tmdbid, imdbid, pi, pn, lang, subt, plex, az, type, anime, regular, music, album, artist, albumArtist, actors, director, collection, genre, genres, languages, runtime, certification, rating, votes, vc, ac, cf, vf, hpi, af, channels, resolution, dim, bitdepth, bitrate, kbps, khz, ws, sdhd, source, tags, s3d, group, original, info, info.network, info.status, info.productionCompanies, info.productionCountries, info.certifications, info.certifications.DE, omdb.rating, omdb.votes, localize.deu.n, localize.deu.t, localize.zho.n, localize.zho.t, order.airdate.sxe, order.dvd.sxe, fn, ext, mediaType, mediaPath, file, file.name, folder, folder.name, mediaTitle, audioLanguages, textLanguages, duration, seconds, minutes, bytes, megabytes, gigabytes, crc32, media.title, media.collection, media.season, media.part, media.partID, media.genre, media.contentType, media.description, media.lyrics, video[0].codecID, video[0].frameRate, video[0].displayAspectRatioString, video[0].scanType, audio.language, audio[0].bitRateString, audio[0].language, text.language, text[0].language, text[0].codecInfo +expressions: n, y, s, e, sxe, s00e00, t, d, startdate, absolute, es, e00, sy, sc, di, dc, age, special, episode, series, primaryTitle, alias, movie, tmdbid, imdbid, pi, pn, lang, subt, plex, az, type, anime, regular, music, album, artist, albumArtist, actors, director, collection, genre, genres, languages, runtime, certification, rating, votes, vc, ac, cf, vf, hpi, af, channels, resolution, dim, bitdepth, bitrate, kbps, khz, ws, sdhd, source, tags, s3d, group, original, info, info.network, info.status, info.productionCompanies, info.productionCountries, info.certifications, info.certifications.DE, omdb.rating, omdb.votes, localize.deu.n, localize.deu.t, localize.zho.n, localize.zho.t, order.airdate.sxe, order.dvd.sxe, fn, ext, mediaType, mediaPath, file, file.name, folder, folder.name, mediaTitle, audioLanguages, textLanguages, duration, seconds, minutes, bytes, megabytes, gigabytes, crc32, media.title, media.collection, media.season, media.part, media.partID, media.genre, media.contentType, media.description, media.lyrics, video[0].codecID, video[0].frameRate, video[0].displayAspectRatioString, video[0].scanType, audio.language, audio[0].bitRateString, audio[0].language, text.language, text[0].language, text[0].codecInfo, exif.make, exif.model, location