diff --git a/source/ehcache.xml b/source/ehcache.xml
index e77d5595..6779c8bd 100644
--- a/source/ehcache.xml
+++ b/source/ehcache.xml
@@ -1,28 +1,9 @@
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/source/net/filebot/Cache.java b/source/net/filebot/Cache.java
index ce68e380..065c810d 100644
--- a/source/net/filebot/Cache.java
+++ b/source/net/filebot/Cache.java
@@ -1,13 +1,16 @@
package net.filebot;
+import static java.nio.charset.StandardCharsets.*;
import static net.filebot.Logging.*;
import java.io.Serializable;
+import java.net.URL;
import java.time.Duration;
import java.util.Arrays;
-import java.util.concurrent.Callable;
import java.util.function.Predicate;
+import net.filebot.web.CachedResource2;
+import net.filebot.web.FloodLimit;
import net.sf.ehcache.Element;
public class Cache {
@@ -24,51 +27,60 @@ public class Cache {
public Object get(Object key) {
try {
- return cache.get(key).getObjectValue();
+ Element element = cache.get(key);
+ if (element != null) {
+ return element.getObjectValue();
+ }
} catch (Exception e) {
- debug.warning(format("Bad cache state: %s => %s", key, e));
+ e.printStackTrace();
+ debug.warning(format("Cache get: %s => %s", key, e));
}
return null;
}
- public Object computeIf(Object key, Predicate condition, Callable> callable) throws Exception {
+ public Object computeIf(Object key, Predicate condition, Compute> compute) throws Exception {
// get if present
+ Element element = null;
try {
- Element element = cache.get(key);
+ element = cache.get(key);
if (element != null && condition.test(element)) {
return element.getObjectValue();
}
} catch (Exception e) {
- debug.warning(format("Bad cache state: %s => %s", key, e));
+ debug.warning(format("Cache get: %s => %s", key, e));
}
// compute if absent
- Object value = callable.call();
+ Object value = compute.apply(element);
try {
cache.put(new Element(key, value));
} catch (Exception e) {
- debug.warning(format("Bad cache state: %s => %s", key, e));
+ debug.warning(format("Cache put: %s => %s", key, e));
}
return value;
}
- public Object computeIfAbsent(Object key, Callable> callable) throws Exception {
- return computeIf(key, Element::isExpired, callable);
+ public Object computeIfAbsent(Object key, Compute> compute) throws Exception {
+ return computeIf(key, isAbsent(), compute);
}
- public Object computeIfStale(Object key, Duration expirationTime, Callable> callable) throws Exception {
- return computeIf(key, isStale(expirationTime), callable);
+ public Object computeIfStale(Object key, Duration expirationTime, Compute> compute) throws Exception {
+ return computeIf(key, isStale(expirationTime), compute);
}
- private Predicate isStale(Duration expirationTime) {
- return (element) -> element.isExpired() || System.currentTimeMillis() - element.getLatestOfCreationAndUpdateTime() < expirationTime.toMillis();
+ public Predicate isAbsent() {
+ return (element) -> element.getObjectValue() == null;
+ }
+
+ public Predicate isStale(Duration expirationTime) {
+ return (element) -> System.currentTimeMillis() - element.getLatestOfCreationAndUpdateTime() < expirationTime.toMillis();
}
public void put(Object key, Object value) {
try {
cache.put(new Element(key, value));
} catch (Exception e) {
- debug.warning(format("Bad cache state: %s => %s", key, e));
+ debug.warning(format("Cache put: %s => %s", key, e));
}
}
@@ -76,10 +88,15 @@ public class Cache {
try {
cache.remove(key);
} catch (Exception e) {
- debug.warning(format("Bad cache state: %s => %s", key, e));
+ debug.warning(format("Cache remove: %s => %s", key, e));
}
}
+ @FunctionalInterface
+ public interface Compute {
+ R apply(Element element) throws Exception;
+ }
+
@Deprecated
public T get(Object key, Class type) {
return type.cast(get(key));
@@ -114,4 +131,8 @@ public class Cache {
}
}
+ public CachedResource2 resource(String url, Duration expirationTime, FloodLimit limit) {
+ return new CachedResource2(url, URL::new, CachedResource2.fetchIfModified(limit), CachedResource2.decode(UTF_8), expirationTime, this);
+ }
+
}
diff --git a/source/net/filebot/format/MediaBindingBean.java b/source/net/filebot/format/MediaBindingBean.java
index 8ff44a30..da38f5fa 100644
--- a/source/net/filebot/format/MediaBindingBean.java
+++ b/source/net/filebot/format/MediaBindingBean.java
@@ -447,7 +447,7 @@ public class MediaBindingBean {
// calculate checksum from file
Cache cache = Cache.getCache("crc32", CacheType.Ephemeral);
- return (String) cache.computeIfAbsent(inferredMediaFile, () -> crc32(inferredMediaFile));
+ return (String) cache.computeIfAbsent(inferredMediaFile, element -> crc32(inferredMediaFile));
}
@Define("fn")
diff --git a/source/net/filebot/web/CachedResource2.java b/source/net/filebot/web/CachedResource2.java
new file mode 100644
index 00000000..874340ce
--- /dev/null
+++ b/source/net/filebot/web/CachedResource2.java
@@ -0,0 +1,123 @@
+package net.filebot.web;
+
+import static net.filebot.Logging.*;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+
+import net.filebot.Cache;
+
+public class CachedResource2 {
+
+ public static final int DEFAULT_RETRY_LIMIT = 2;
+ public static final Duration DEFAULT_RETRY_DELAY = Duration.ofSeconds(2);
+
+ protected final K key;
+
+ protected final Source source;
+ protected final Fetch fetch;
+ protected final Parse parse;
+
+ protected final Duration expirationTime;
+
+ protected final int retryCountLimit;
+ protected final long retryWaitTime;
+
+ protected final Cache cache;
+
+ public CachedResource2(K key, Source source, Fetch fetch, Parse parse, Duration expirationTime, Cache cache) {
+ this(key, source, fetch, parse, DEFAULT_RETRY_LIMIT, DEFAULT_RETRY_DELAY, expirationTime, cache);
+ }
+
+ public CachedResource2(K key, Source source, Fetch fetch, Parse parse, int retryCountLimit, Duration retryWaitTime, Duration expirationTime, Cache cache) {
+ this.key = key;
+ this.source = source;
+ this.fetch = fetch;
+ this.parse = parse;
+ this.expirationTime = expirationTime;
+ this.retryCountLimit = retryCountLimit;
+ this.retryWaitTime = retryWaitTime.toMillis();
+ this.cache = cache;
+ }
+
+ @SuppressWarnings("unchecked")
+ public synchronized R get() throws Exception {
+ return (R) cache.computeIfStale(key, expirationTime, element -> {
+ URL resource = source.source(key);
+ long lastModified = element == null ? 0 : element.getLatestOfCreationAndUpdateTime();
+
+ debug.fine(format("Fetch %s (If-Modified-Since: %tc)", resource, lastModified));
+
+ try {
+ ByteBuffer data = retry(() -> fetch.fetch(resource, lastModified), retryCountLimit, lastModified);
+
+ // 304 Not Modified
+ if (data == null && element != null && element.getObjectValue() != null) {
+ return element.getObjectValue();
+ }
+
+ return parse.parse(data);
+ } catch (IOException e) {
+ debug.fine(format("Fetch failed => %s", e));
+
+ // use previously cached data if possible
+ if (element == null || element.getObjectValue() == null) {
+ throw e;
+ }
+ return element.getObjectKey();
+ }
+ });
+ }
+
+ protected T retry(Callable callable, int retryCount, long retryWaitTime) throws Exception {
+ try {
+ return callable.call();
+ } catch (FileNotFoundException e) {
+ // resource does not exist, do not retry
+ throw e;
+ } catch (IOException e) {
+ // retry or rethrow exception
+ if (retryCount > 0) {
+ throw e;
+ }
+ Thread.sleep(retryWaitTime);
+ return retry(callable, retryCount - 1, retryWaitTime * 2);
+ }
+ }
+
+ @FunctionalInterface
+ public interface Source {
+ URL source(K key) throws Exception;
+ }
+
+ @FunctionalInterface
+ public interface Fetch {
+ ByteBuffer fetch(URL url, long lastModified) throws Exception;
+ }
+
+ @FunctionalInterface
+ public interface Parse {
+ R parse(ByteBuffer bytes) throws Exception;
+ }
+
+ public static Parse decode(Charset charset) {
+ return (bb) -> charset.decode(bb).toString();
+ }
+
+ public static Fetch fetchIfModified(FloodLimit limit) {
+ return (url, lastModified) -> {
+ try {
+ limit.acquirePermit();
+ return WebRequest.fetchIfModified(url, lastModified);
+ } catch (FileNotFoundException e) {
+ return ByteBuffer.allocate(0);
+ }
+ };
+ }
+
+}
diff --git a/source/net/filebot/web/OMDbClient.java b/source/net/filebot/web/OMDbClient.java
index bf08a748..ced8575d 100644
--- a/source/net/filebot/web/OMDbClient.java
+++ b/source/net/filebot/web/OMDbClient.java
@@ -2,16 +2,13 @@ package net.filebot.web;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
+import static net.filebot.Logging.*;
import static net.filebot.util.JsonUtilities.*;
import static net.filebot.util.StringUtilities.*;
import static net.filebot.web.WebRequest.*;
import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.net.URL;
-import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
+import java.time.Duration;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
@@ -27,27 +24,22 @@ import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
-import java.util.logging.Level;
-import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.Icon;
+import net.filebot.Cache;
+import net.filebot.CacheType;
import net.filebot.ResourceManager;
import net.filebot.web.TMDbClient.MovieInfo;
import net.filebot.web.TMDbClient.MovieInfo.MovieProperty;
import net.filebot.web.TMDbClient.Person;
-import net.sf.ehcache.Cache;
-import net.sf.ehcache.CacheManager;
public class OMDbClient implements MovieIdentificationService {
private static final FloodLimit REQUEST_LIMIT = new FloodLimit(20, 10, TimeUnit.SECONDS);
- private final String protocol = "http";
- private final String host = "www.omdbapi.com";
-
@Override
public String getName() {
return "OMDb";
@@ -70,7 +62,7 @@ public class OMDbClient implements MovieIdentificationService {
}
@Override
- public List searchMovie(String query, Locale locale) throws IOException {
+ public List searchMovie(String query, Locale locale) throws Exception {
// query by name with year filter if possible
Matcher nameYear = Pattern.compile("(.+)\\b(19\\d{2}|20\\d{2})$").matcher(query);
if (nameYear.matches()) {
@@ -80,14 +72,14 @@ public class OMDbClient implements MovieIdentificationService {
}
}
- public List searchMovie(String movieName, int movieYear) throws IOException {
+ public List searchMovie(String movieName, int movieYear) throws Exception {
Map param = new LinkedHashMap(2);
param.put("s", movieName);
if (movieYear > 0) {
param.put("y", movieYear);
}
- Map, ?> response = request(param, REQUEST_LIMIT);
+ Map, ?> response = request(param);
List result = new ArrayList();
for (Object it : getArray(response, "Search")) {
@@ -141,40 +133,16 @@ public class OMDbClient implements MovieIdentificationService {
throw new UnsupportedOperationException();
}
- public Map, ?> request(Map parameters, final FloodLimit limit) throws IOException {
- URL url = new URL(protocol, host, "/?" + encodeParameters(parameters, true));
+ public Map, ?> request(Map parameters) throws Exception {
+ String url = "http://www.omdbapi.com/?" + encodeParameters(parameters, true);
- CachedResource json = new CachedResource(url.toString(), String.class, CachedResource.ONE_WEEK) {
+ Cache cache = Cache.getCache(getName(), CacheType.Weekly);
+ String json = cache.resource(url, Duration.ofDays(7), REQUEST_LIMIT).get();
- @Override
- public String process(ByteBuffer data) throws Exception {
- return Charset.forName("UTF-8").decode(data).toString();
- }
-
- @Override
- protected ByteBuffer fetchData(URL url, long lastModified) throws IOException {
- try {
- if (limit != null) {
- limit.acquirePermit();
- }
- return super.fetchData(url, lastModified);
- } catch (FileNotFoundException e) {
- return ByteBuffer.allocate(0);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- protected Cache getCache() {
- return CacheManager.getInstance().getCache("web-datasource-lv2");
- }
- };
-
- return asMap(readJson(json.get()));
+ return asMap(readJson(json));
}
- public Map getMovieInfo(Integer i, String t, String y, boolean tomatoes) throws IOException {
+ public Map getMovieInfo(Integer i, String t, String y, boolean tomatoes) throws Exception {
// e.g. http://www.imdbapi.com/?i=tt0379786&r=xml&tomatoes=true
Map param = new LinkedHashMap(2);
if (i != null) {
@@ -188,10 +156,10 @@ public class OMDbClient implements MovieIdentificationService {
}
param.put("tomatoes", String.valueOf(tomatoes));
- return getInfoMap(request(param, REQUEST_LIMIT));
+ return getInfoMap(request(param));
}
- public MovieInfo getMovieInfo(Movie movie) throws IOException {
+ public MovieInfo getMovieInfo(Movie movie) throws Exception {
Map data = movie.getImdbId() > 0 ? getMovieInfo(movie.getImdbId(), null, null, false) : getMovieInfo(null, movie.getName(), String.valueOf(movie.getYear()), false);
// sanity check
@@ -243,7 +211,7 @@ public class OMDbClient implements MovieIdentificationService {
}
}
} catch (DateTimeParseException e) {
- Logger.getLogger(OMDbClient.class.getName()).log(Level.WARNING, String.format("Bad date: %s: %s", value, e.getMessage()));
+ debug.warning(format("Bad date: %s =~ %s => %s", value, format, e));
}
}
return null;
diff --git a/test/net/filebot/web/AcoustIDClientTest.java b/test/net/filebot/web/AcoustIDClientTest.java
index 38309d89..fa183243 100644
--- a/test/net/filebot/web/AcoustIDClientTest.java
+++ b/test/net/filebot/web/AcoustIDClientTest.java
@@ -4,10 +4,6 @@ import static org.junit.Assert.*;
import java.io.IOException;
-import net.sf.ehcache.CacheManager;
-
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
import org.junit.Test;
public class AcoustIDClientTest {
@@ -38,10 +34,4 @@ public class AcoustIDClientTest {
assertEquals("聽媽媽的話", info.getTitle());
}
- @BeforeClass
- @AfterClass
- public static void clearCache() {
- CacheManager.getInstance().clearAll();
- }
-
}
diff --git a/test/net/filebot/web/OMDbClientTest.java b/test/net/filebot/web/OMDbClientTest.java
index dbf6bc0d..7ae8d5af 100644
--- a/test/net/filebot/web/OMDbClientTest.java
+++ b/test/net/filebot/web/OMDbClientTest.java
@@ -24,12 +24,12 @@ public class OMDbClientTest {
@Test
public void searchMovie2() throws Exception {
- List results = client.searchMovie("The Illusionist", null);
+ List results = client.searchMovie("The Terminator", null);
Movie movie = results.get(0);
- assertEquals("The Illusionist", movie.getName());
- assertEquals(2006, movie.getYear());
- assertEquals(443543, movie.getImdbId(), 0);
+ assertEquals("The Terminator", movie.getName());
+ assertEquals(1984, movie.getYear());
+ assertEquals(88247, movie.getImdbId());
}
@Test