diff --git a/source/net/filebot/format/AssociativeEnumObject.java b/source/net/filebot/format/AssociativeEnumObject.java new file mode 100644 index 00000000..74d15cde --- /dev/null +++ b/source/net/filebot/format/AssociativeEnumObject.java @@ -0,0 +1,171 @@ +package net.filebot.format; + +import static net.filebot.util.RegularExpressions.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import groovy.lang.GroovyObjectSupport; + +public class AssociativeEnumObject extends GroovyObjectSupport implements List { + + private final Map values; + + public AssociativeEnumObject(Map values) { + this.values = values; + } + + protected String definingKey(Object key) { + // letters and digits are defining, everything else will be ignored + return NON_WORD.matcher(key.toString()).replaceAll("").toLowerCase(); + } + + @Override + public Object getProperty(String name) { + return getValue(definingKey(name)).orElseGet(() -> super.getProperty(name)); + } + + private Optional getValue(String key) { + return values.keySet().stream().filter(k -> key.equals(definingKey(k))).findFirst().map(values::get).map(Object.class::cast); + } + + @Override + public void setProperty(String name, Object value) { + throw new UnsupportedOperationException(); + } + + public Set keySet() { + return values.keySet(); + } + + @Override + public String toString() { + return values.values().toString(); + } + + public List toList() { + return new ArrayList(values.values()); + } + + @Override + public Iterator iterator() { + return toList().iterator(); + } + + @Override + public Object get(int index) { + return toList().get(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return toList().subList(fromIndex, toIndex); + } + + @Override + public int size() { + return values.size(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return values.values().contains(o); + } + + @Override + public Object[] toArray() { + return values.values().toArray(); + } + + @Override + public T[] toArray(T[] a) { + return values.values().toArray(a); + } + + @Override + public boolean add(Object e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + return values.values().containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public Object set(int index, Object element) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int index, Object element) { + throw new UnsupportedOperationException(); + } + + @Override + public Object remove(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Object o) { + return toList().indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return toList().lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return toList().listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return toList().listIterator(index); + } + +} diff --git a/source/net/filebot/format/AssociativeScriptObject.java b/source/net/filebot/format/AssociativeScriptObject.java index b29af7f2..2c836f67 100644 --- a/source/net/filebot/format/AssociativeScriptObject.java +++ b/source/net/filebot/format/AssociativeScriptObject.java @@ -1,6 +1,7 @@ package net.filebot.format; +import static net.filebot.util.RegularExpressions.*; import java.util.AbstractMap; import java.util.AbstractSet; @@ -13,17 +14,14 @@ import java.util.TreeSet; import groovy.lang.GroovyObjectSupport; - public class AssociativeScriptObject extends GroovyObjectSupport implements Iterable> { private final Map properties; - public AssociativeScriptObject(Map properties) { this.properties = new LenientLookup(properties); } - /** * Get the property with the given name. * @@ -37,26 +35,22 @@ public class AssociativeScriptObject extends GroovyObjectSupport implements Iter return properties.get(name); } - @Override public void setProperty(String name, Object value) { // ignore, object is immutable } - @Override public Iterator> iterator() { return properties.entrySet().iterator(); } - @Override public String toString() { // all the properties in alphabetic order return new TreeSet(properties.keySet()).toString(); } - /** * Map allowing look-up of values by a fault-tolerant key as specified by the defining key. * @@ -65,7 +59,6 @@ public class AssociativeScriptObject extends GroovyObjectSupport implements Iter private final Map> lookup = new HashMap>(); - public LenientLookup(Map source) { // populate lookup map for (Entry entry : source.entrySet()) { @@ -73,19 +66,16 @@ public class AssociativeScriptObject extends GroovyObjectSupport implements Iter } } - protected String definingKey(Object key) { // letters and digits are defining, everything else will be ignored - return key.toString().replaceAll("[^\\p{Alnum}]", "").toLowerCase(); + return NON_WORD.matcher(key.toString()).replaceAll("").toLowerCase(); } - @Override public boolean containsKey(Object key) { return lookup.containsKey(definingKey(key)); } - @Override public Object get(Object key) { Entry entry = lookup.get(definingKey(key)); @@ -96,7 +86,6 @@ public class AssociativeScriptObject extends GroovyObjectSupport implements Iter return null; } - @Override public Set> entrySet() { return new AbstractSet>() { @@ -106,7 +95,6 @@ public class AssociativeScriptObject extends GroovyObjectSupport implements Iter return (Iterator) lookup.values().iterator(); } - @Override public int size() { return lookup.size(); diff --git a/source/net/filebot/format/MediaBindingBean.java b/source/net/filebot/format/MediaBindingBean.java index 51542445..a319352b 100644 --- a/source/net/filebot/format/MediaBindingBean.java +++ b/source/net/filebot/format/MediaBindingBean.java @@ -844,9 +844,14 @@ public class MediaBindingBean { return new AssociativeScriptObject(new ImageMetadata(getMediaFile()).snapshot(t -> t.getTagName())); } + @Define("camera") + public AssociativeEnumObject getCamera() throws Exception { + return new ImageMetadata(getMediaFile()).getCameraModel().map(AssociativeEnumObject::new).orElse(null); + } + @Define("location") - public List getLocation() throws Exception { - return new ImageMetadata(getMediaFile()).getLocationTaken().orElse(null); + public AssociativeEnumObject getLocation() throws Exception { + return new ImageMetadata(getMediaFile()).getLocationTaken().map(AssociativeEnumObject::new).orElse(null); } @Define("artist") diff --git a/source/net/filebot/mediainfo/ImageMetadata.java b/source/net/filebot/mediainfo/ImageMetadata.java index 861ba932..3385fb09 100644 --- a/source/net/filebot/mediainfo/ImageMetadata.java +++ b/source/net/filebot/mediainfo/ImageMetadata.java @@ -1,8 +1,6 @@ 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.util.JsonUtilities.*; @@ -14,9 +12,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; 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; @@ -77,39 +73,62 @@ public class ImageMetadata { }); } - public Optional> getLocationTaken() { + public Optional> getCameraModel() { + return extract(m -> { + ExifIFD0Directory directory = m.getFirstDirectoryOfType(ExifIFD0Directory.class); + String maker = directory.getDescription(ExifIFD0Directory.TAG_MAKE); + String model = directory.getDescription(ExifIFD0Directory.TAG_MODEL); + + Map camera = new EnumMap(CameraProperty.class); + if (maker != null) { + camera.put(CameraProperty.maker, maker); + } + if (model != null) { + camera.put(CameraProperty.model, model); + } + return camera; + }).filter(m -> m.size() > 0); + } + + public enum CameraProperty { + maker, model; + } + + public Optional> getLocationTaken() { return extract(m -> m.getFirstDirectoryOfType(GpsDirectory.class).getGeoLocation()).map(this::locate); } - protected List locate(GeoLocation location) { + protected Map 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); + Map address = new EnumMap(AddressComponent.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); - }); + if (name != null) { + for (Object type : getArray(a, "types")) { + stream(AddressComponent.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 + return address; } catch (Exception e) { debug.warning(e::toString); } - return emptyList(); + return null; } - private enum AddressComponents { - country, administrative_area_level_1, administrative_area_level_2, administrative_area_level_3, administrative_area_level_4, sublocality, neighborhood; + public enum AddressComponent { + country, administrative_area_level_1, administrative_area_level_2, administrative_area_level_3, administrative_area_level_4, sublocality, neighborhood, route; } protected Optional extract(Function extract) { diff --git a/source/net/filebot/ui/rename/BindingDialog.properties b/source/net/filebot/ui/rename/BindingDialog.properties index 6e2f5f0d..f714ac38 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, exif.make, exif.model, location +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, camera, camera.maker, camera.model, location, location.country diff --git a/source/net/filebot/util/RegularExpressions.java b/source/net/filebot/util/RegularExpressions.java index da47fdfa..5e6bee5e 100644 --- a/source/net/filebot/util/RegularExpressions.java +++ b/source/net/filebot/util/RegularExpressions.java @@ -8,6 +8,7 @@ public class RegularExpressions { public static final Pattern DIGIT = compile("\\d+"); public static final Pattern NON_DIGIT = compile("\\D+"); + public static final Pattern NON_WORD = compile("[\\P{Alnum}]+"); public static final Pattern PIPE = compile("|", LITERAL); public static final Pattern EQUALS = compile("=", LITERAL);