From 3db219f1fbcc5304b2ff4a2d1c9f961faa78d5fb Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Sun, 5 May 2019 12:23:31 +0700 Subject: [PATCH] Experiment with artwork thumbnail support --- source/net/filebot/ThumbnailServices.java | 113 ++++++++++++++++++ source/net/filebot/WebServices.java | 16 ++- source/net/filebot/ui/SelectDialog.java | 12 +- .../net/filebot/ui/rename/BlankThumbnail.java | 19 ++- .../filebot/ui/rename/EpisodeListMatcher.java | 29 ++--- .../net/filebot/ui/rename/MovieMatcher.java | 25 +++- source/net/filebot/web/ThumbnailProvider.java | 80 +------------ 7 files changed, 175 insertions(+), 119 deletions(-) create mode 100644 source/net/filebot/ThumbnailServices.java diff --git a/source/net/filebot/ThumbnailServices.java b/source/net/filebot/ThumbnailServices.java new file mode 100644 index 00000000..0889de46 --- /dev/null +++ b/source/net/filebot/ThumbnailServices.java @@ -0,0 +1,113 @@ +package net.filebot; + +import static java.nio.charset.StandardCharsets.*; +import static java.util.stream.Collectors.*; +import static net.filebot.Logging.*; +import static net.filebot.util.FileUtilities.*; +import static net.filebot.util.RegularExpressions.*; + +import java.net.URI; +import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +import javax.swing.Icon; +import javax.swing.ImageIcon; + +import org.tukaani.xz.XZInputStream; + +import net.filebot.web.SearchResult; +import net.filebot.web.ThumbnailProvider; + +public enum ThumbnailServices implements ThumbnailProvider { + + TheTVDB, TheMovieDB; + + protected String getResource(String file) { + return "https://api.filebot.net/images/" + name().toLowerCase() + "/thumb/poster/" + file; + } + + protected Cache getCache() { + return Cache.getCache("thumbnail_" + ordinal(), CacheType.Persistent); + } + + protected Set getIndex() throws Exception { + byte[] bytes = getCache().bytes("index.txt.xz", n -> new URL(getResource(n)), XZInputStream::new).expire(Cache.ONE_MONTH).get(); + + // all data files are UTF-8 encoded XZ compressed text files + return NEWLINE.splitAsStream(UTF_8.decode(ByteBuffer.wrap(bytes))).filter(s -> s.length() > 0).map(Integer::parseInt).collect(toSet()); + } + + private final Resource> index = Resource.lazy(this::getIndex); + + public byte[][] getThumbnails(int[] ids) throws Exception { + Cache cache = getCache(); + byte[][] response = new byte[ids.length][]; + + synchronized (index) { + // check cache + for (int i = 0; i < response.length; i++) { + response[i] = (byte[]) cache.get(ids[i]); + } + + // create if necessary + CompletableFuture>[] request = new CompletableFuture[ids.length]; + Resource http = Resource.lazy(HttpClient::newHttpClient); + + for (int i = 0; i < response.length; i++) { + if (response[i] == null && index.get().contains(ids[i])) { + URI r = URI.create(getResource(ids[i] + ".png")); + request[i] = http.get().sendAsync(HttpRequest.newBuilder(r).build(), BodyHandlers.ofByteArray()); + + debug.fine(format("Request %s", r)); + } + } + + for (int i = 0; i < response.length; i++) { + if (request[i] != null) { + try { + HttpResponse r = request[i].get(); + + response[i] = r.statusCode() == 200 ? r.body() : new byte[0]; + cache.put(ids[i], response[i]); + + debug.finest(format("Received %s (%s)", formatSize(response[i].length), r.uri())); + } catch (Exception e) { + debug.warning(e::toString); + } + } + } + + return response; + } + } + + @Override + public Map getThumbnails(List keys) throws Exception { + int[] ids = keys.stream().mapToInt(SearchResult::getId).toArray(); + byte[][] thumbnails = getThumbnails(ids); + + Map icons = new HashMap<>(thumbnails.length); + for (int i = 0; i < thumbnails.length; i++) { + if (thumbnails[i] != null && thumbnails[i].length > 0) { + try { + icons.put(keys.get(i), new ImageIcon(thumbnails[i])); + } catch (Exception e) { + debug.log(Level.SEVERE, e, e::toString); + } + } + } + + return icons; + } + +} \ No newline at end of file diff --git a/source/net/filebot/WebServices.java b/source/net/filebot/WebServices.java index c4f64025..c9f2cd6b 100644 --- a/source/net/filebot/WebServices.java +++ b/source/net/filebot/WebServices.java @@ -22,6 +22,8 @@ import java.util.concurrent.Future; import java.util.logging.Level; import java.util.stream.Stream; +import javax.swing.Icon; + import net.filebot.media.LocalDatasource; import net.filebot.similarity.MetricAvg; import net.filebot.web.AcoustIDClient; @@ -45,6 +47,7 @@ import net.filebot.web.TMDbTVClient; import net.filebot.web.TVMazeClient; import net.filebot.web.TheTVDBClient; import net.filebot.web.TheTVDBSearchResult; +import net.filebot.web.ThumbnailProvider; import net.filebot.web.VideoHashSubtitleService; /** @@ -130,7 +133,7 @@ public final class WebServices { public static final ExecutorService requestThreadPool = Executors.newCachedThreadPool(); - public static class TMDbClientWithLocalSearch extends TMDbClient { + public static class TMDbClientWithLocalSearch extends TMDbClient implements ThumbnailProvider { public TMDbClientWithLocalSearch(String apikey) { super(apikey); @@ -189,9 +192,13 @@ public final class WebServices { return new ArrayList<>(movies); } + @Override + public Map getThumbnails(List keys) throws Exception { + return ThumbnailServices.TheMovieDB.getThumbnails(keys); + } } - public static class TheTVDBClientWithLocalSearch extends TheTVDBClient { + public static class TheTVDBClientWithLocalSearch extends TheTVDBClient implements ThumbnailProvider { public TheTVDBClientWithLocalSearch(String apikey) { super(apikey); @@ -226,6 +233,11 @@ public final class WebServices { return sortBySimilarity(results.values(), singleton(query), getSeriesMatchMetric()); } + + @Override + public Map getThumbnails(List keys) throws Exception { + return ThumbnailServices.TheTVDB.getThumbnails(keys); + } } public static class AnidbClientWithLocalSearch extends AnidbClient { diff --git a/source/net/filebot/ui/SelectDialog.java b/source/net/filebot/ui/SelectDialog.java index 471a6d42..f99da691 100644 --- a/source/net/filebot/ui/SelectDialog.java +++ b/source/net/filebot/ui/SelectDialog.java @@ -10,7 +10,7 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.io.File; import java.util.Collection; -import java.util.Map; +import java.util.function.Function; import java.util.prefs.Preferences; import javax.swing.Action; @@ -40,18 +40,18 @@ public class SelectDialog extends JDialog { private JList list; private String command = null; - private Map icons; + private Function icon; public SelectDialog(Component parent, Collection options) { this(parent, options, null, false, false, null); } - public SelectDialog(Component parent, Collection options, Map icons, boolean autoRepeatEnabled, boolean autoRepeatSelected, JComponent header) { + public SelectDialog(Component parent, Collection options, Function icon, boolean autoRepeatEnabled, boolean autoRepeatSelected, JComponent header) { super(getWindow(parent), "Select", ModalityType.DOCUMENT_MODAL); setDefaultCloseOperation(DISPOSE_ON_CLOSE); // enable icons - this.icons = icons; + this.icon = icon; // initialize list list = new JList(options.toArray()); @@ -117,8 +117,8 @@ public class SelectDialog extends JDialog { render.setToolTipText(null); } - if (icons != null) { - render.setIcon(icons.get(value)); + if (icon != null) { + render.setIcon(icon.apply((T) value)); } } diff --git a/source/net/filebot/ui/rename/BlankThumbnail.java b/source/net/filebot/ui/rename/BlankThumbnail.java index fc82b4f2..d2d386d1 100644 --- a/source/net/filebot/ui/rename/BlankThumbnail.java +++ b/source/net/filebot/ui/rename/BlankThumbnail.java @@ -2,10 +2,9 @@ package net.filebot.ui.rename; import static net.filebot.ui.ThemeSupport.*; +import java.awt.Color; import java.awt.Component; import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Paint; import javax.swing.Icon; @@ -16,13 +15,13 @@ public class BlankThumbnail implements Icon { private int width; private int height; - private Paint fill; - private Paint draw; + private Color fill; + private Color draw; private float squeezeX; private float squeezeY; - public BlankThumbnail(int width, int height, Paint fill, Paint draw, float squeezeX, float squeezeY) { + public BlankThumbnail(int width, int height, Color fill, Color draw, float squeezeX, float squeezeY) { this.width = width; this.height = height; this.fill = fill; @@ -33,18 +32,16 @@ public class BlankThumbnail implements Icon { @Override public void paintIcon(Component c, Graphics g, int x, int y) { - Graphics2D g2d = (Graphics2D) g; - int w = (int) (width * squeezeX); int h = (int) (height * squeezeY); x = (int) (x + (width - w) / 2); y = (int) (y + (width - h) / 2); - g2d.setPaint(fill); - g2d.fillRect(x, y, w, h); + g.setColor(fill); + g.fillRect(x, y, w, h); - g2d.setPaint(draw); - g2d.drawRect(x, y, w, h); + g.setColor(draw); + g.drawRect(x, y, w, h); } @Override diff --git a/source/net/filebot/ui/rename/EpisodeListMatcher.java b/source/net/filebot/ui/rename/EpisodeListMatcher.java index ce0aba58..51d7a37a 100644 --- a/source/net/filebot/ui/rename/EpisodeListMatcher.java +++ b/source/net/filebot/ui/rename/EpisodeListMatcher.java @@ -19,7 +19,6 @@ import java.awt.Component; import java.io.File; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -32,13 +31,13 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.function.Function; import java.util.logging.Level; import java.util.prefs.Preferences; import java.util.regex.Pattern; import javax.swing.Action; import javax.swing.Icon; -import javax.swing.ImageIcon; import javax.swing.JLabel; import net.filebot.Cache; @@ -51,7 +50,6 @@ import net.filebot.web.Episode; import net.filebot.web.EpisodeListProvider; import net.filebot.web.SearchResult; import net.filebot.web.SortOrder; -import net.filebot.web.TheTVDBClient; import net.filebot.web.ThumbnailProvider; class EpisodeListMatcher implements AutoCompleteMatcher { @@ -236,7 +234,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } // prepare thumbnail images - Map thumbnails = getThumbnails(options); + Function thumbnail = thumbnail(options); // show selection dialog on EDT Callable showSelectDialog = () -> { @@ -244,7 +242,7 @@ class EpisodeListMatcher implements AutoCompleteMatcher { header.setBorder(createCompoundBorder(createTitledBorder(""), createEmptyBorder(3, 3, 3, 3))); // multiple results have been found, user must select one - SelectDialog selectDialog = new SelectDialog(parent, options, thumbnails, true, false, header.getText().isEmpty() ? null : header); + SelectDialog selectDialog = new SelectDialog(parent, options, thumbnail, true, false, header.getText().isEmpty() ? null : header); selectDialog.setTitle(provider.getName()); selectDialog.getMessageLabel().setText("Select best match for \"" + escapeHTML(query) + "\":"); selectDialog.getCancelAction().putValue(Action.NAME, "Skip"); @@ -293,23 +291,12 @@ class EpisodeListMatcher implements AutoCompleteMatcher { } } - protected Map getThumbnails(List options) { - if (provider instanceof TheTVDBClient) { + protected Function thumbnail(List options) { + if (provider instanceof ThumbnailProvider) { try { - int[] ids = options.stream().mapToInt(SearchResult::getId).toArray(); - byte[][] thumbnails = ThumbnailProvider.TheTVDB.getThumbnails(ids); - - Map icons = new HashMap<>(ids.length); - for (int i = 0; i < ids.length; i++) { - if (thumbnails[i] != null && thumbnails[i].length > 0) { - icons.put(options.get(i), new ImageIcon(thumbnails[i])); - } else { - icons.put(options.get(i), BlankThumbnail.BLANK_POSTER); - } - } - - if (icons.size() > 0) { - return icons; + Map thumbnails = ((ThumbnailProvider) provider).getThumbnails(options); + if (thumbnails.size() > 0) { + return key -> thumbnails.getOrDefault(key, BlankThumbnail.BLANK_POSTER); } } catch (Exception e) { debug.log(Level.SEVERE, e, e::toString); diff --git a/source/net/filebot/ui/rename/MovieMatcher.java b/source/net/filebot/ui/rename/MovieMatcher.java index 1ffd9253..8a0b0408 100644 --- a/source/net/filebot/ui/rename/MovieMatcher.java +++ b/source/net/filebot/ui/rename/MovieMatcher.java @@ -34,6 +34,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.function.Function; import java.util.logging.Level; import java.util.prefs.Preferences; @@ -49,7 +50,9 @@ import net.filebot.util.FileUtilities.ParentFilter; import net.filebot.web.Movie; import net.filebot.web.MovieIdentificationService; import net.filebot.web.MoviePart; +import net.filebot.web.SearchResult; import net.filebot.web.SortOrder; +import net.filebot.web.ThumbnailProvider; class MovieMatcher implements AutoCompleteMatcher { @@ -219,7 +222,7 @@ class MovieMatcher implements AutoCompleteMatcher { return matches; } - protected Movie grabMovieName(File movieFile, Collection options, boolean strict, Locale locale, boolean autodetect, Component parent) throws Exception { + protected Movie grabMovieName(File movieFile, List options, boolean strict, Locale locale, boolean autodetect, Component parent) throws Exception { // allow manual user input synchronized (selectionMemory) { if (!strict && (!autodetect || options.isEmpty()) && !(autodetect && autoSelectionMode.size() > 0)) { @@ -280,7 +283,7 @@ class MovieMatcher implements AutoCompleteMatcher { return name; } - protected Movie selectMovie(File movieFile, boolean strict, String userQuery, Collection options, Component parent) throws Exception { + protected Movie selectMovie(File movieFile, boolean strict, String userQuery, List options, Component parent) throws Exception { // just auto-pick singleton results if (options.size() == 1) { return options.iterator().next(); @@ -339,7 +342,7 @@ class MovieMatcher implements AutoCompleteMatcher { } // prepare thumbnail images - Map thumbnails = null; + Function thumbnail = thumbnail(options); // show selection dialog on EDT Callable showSelectDialog = () -> { @@ -348,7 +351,7 @@ class MovieMatcher implements AutoCompleteMatcher { header.setBorder(createCompoundBorder(createTitledBorder(""), createEmptyBorder(3, 3, 3, 3))); // multiple results have been found, user must select one - SelectDialog selectDialog = new SelectDialog(parent, options, thumbnails, true, false, header); + SelectDialog selectDialog = new SelectDialog(parent, options, thumbnail, true, false, header); selectDialog.setTitle(service.getName()); selectDialog.getMessageLabel().setText("Select best match for \"" + escapeHTML(query) + "\":"); @@ -404,6 +407,20 @@ class MovieMatcher implements AutoCompleteMatcher { First, Skip; } + protected Function thumbnail(List options) { + if (service instanceof ThumbnailProvider) { + try { + Map thumbnails = ((ThumbnailProvider) service).getThumbnails((List) options); + if (thumbnails.size() > 0) { + return key -> thumbnails.getOrDefault(key, BlankThumbnail.BLANK_POSTER); + } + } catch (Exception e) { + debug.log(Level.SEVERE, e, e::toString); + } + } + return null; + } + public List> justFetchMovieInfo(Locale locale, Component parent) throws Exception { // require user input String input = showInputDialog("Enter movie name:", "", "Fetch Movie Info", parent); diff --git a/source/net/filebot/web/ThumbnailProvider.java b/source/net/filebot/web/ThumbnailProvider.java index c6d930f7..d388cdaf 100644 --- a/source/net/filebot/web/ThumbnailProvider.java +++ b/source/net/filebot/web/ThumbnailProvider.java @@ -1,82 +1,12 @@ package net.filebot.web; -import static java.nio.charset.StandardCharsets.*; -import static java.util.stream.Collectors.*; -import static net.filebot.Logging.*; -import static net.filebot.util.FileUtilities.*; -import static net.filebot.util.RegularExpressions.*; +import java.util.List; +import java.util.Map; -import java.net.URI; -import java.net.URL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; -import java.nio.ByteBuffer; -import java.util.Set; -import java.util.concurrent.CompletableFuture; +import javax.swing.Icon; -import org.tukaani.xz.XZInputStream; +public interface ThumbnailProvider { -import net.filebot.Cache; -import net.filebot.CacheType; -import net.filebot.Resource; - -public enum ThumbnailProvider { - - TheTVDB, TheMovieDB; - - protected String getResourceLocation(String file) { - return "https://api.filebot.net/images/" + name().toLowerCase() + "/thumb/poster/" + file; - } - - protected Set getIndex() throws Exception { - byte[] bytes = cache.bytes("index.txt.xz", n -> new URL(getResourceLocation(n)), XZInputStream::new).expire(Cache.ONE_MONTH).get(); - - // all data files are UTF-8 encoded XZ compressed text files - return NEWLINE.splitAsStream(UTF_8.decode(ByteBuffer.wrap(bytes))).filter(s -> s.length() > 0).map(Integer::parseInt).collect(toSet()); - } - - private final Resource> index = Resource.lazy(this::getIndex); - - public byte[][] getThumbnails(int[] ids) throws Exception { - synchronized (index) { - CompletableFuture>[] request = new CompletableFuture[ids.length]; - byte[][] response = new byte[ids.length][]; - - // check cache - for (int i = 0; i < response.length; i++) { - response[i] = (byte[]) cache.get(ids[i]); - } - - // create if necessary - Resource http = Resource.lazy(HttpClient::newHttpClient); - - for (int i = 0; i < response.length; i++) { - if (response[i] == null && index.get().contains(ids[i])) { - HttpRequest r = HttpRequest.newBuilder(URI.create(getResourceLocation(ids[i] + ".png"))).build(); - request[i] = http.get().sendAsync(r, BodyHandlers.ofByteArray()); - - debug.fine(format("Request %s", r.uri())); - } - } - - for (int i = 0; i < response.length; i++) { - if (request[i] != null) { - HttpResponse r = request[i].get(); - - response[i] = r.statusCode() == 200 ? r.body() : new byte[0]; - cache.put(ids[i], response[i]); - - debug.finest(format("Received %s (%s)", formatSize(response[i].length), r.uri())); - } - } - - return response; - } - } - - // per instance cache - private final Cache cache = Cache.getCache("thumbnail_" + ordinal(), CacheType.Monthly); + Map getThumbnails(List keys) throws Exception; }