Added {location} binding for dynamically resolving GPS -> Address via maps.googleapis.com
This commit is contained in:
parent
2c7c32472e
commit
2a5ebbc7f1
|
@ -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) {
|
||||
|
|
|
@ -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<String> getLocation() throws Exception {
|
||||
return new ImageMetadata(getMediaFile()).getLocationTaken().orElse(null);
|
||||
}
|
||||
|
||||
@Define("artist")
|
||||
|
|
|
@ -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<String, String> {
|
||||
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<String, String> snapshot(Function<Tag, String> key) throws ImageProcessingException, IOException {
|
||||
Map<String, String> values = new LinkedHashMap<String, String>();
|
||||
|
||||
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<ZonedDateTime> getDateTaken() {
|
||||
return extract(m -> m.getFirstDirectoryOfType(ExifIFD0Directory.class).getDate(ExifSubIFDDirectory.TAG_DATETIME)).map(d -> {
|
||||
return d.toInstant().atZone(ZoneOffset.UTC);
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<List<String>> getLocationTaken() {
|
||||
return extract(m -> m.getFirstDirectoryOfType(GpsDirectory.class).getGeoLocation()).map(this::locate);
|
||||
}
|
||||
|
||||
protected List<String> 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<AddressComponents, String> address = new EnumMap<AddressComponents, String>(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 <T> Optional<T> extract(Function<Metadata, T> 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");
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue