Experiment with artwork thumbnail support

This commit is contained in:
Reinhard Pointner 2019-05-05 12:23:31 +07:00
parent 72f2f68d0e
commit 3db219f1fb
7 changed files with 175 additions and 119 deletions

View File

@ -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<Integer> 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<Set<Integer>> 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<HttpResponse<byte[]>>[] request = new CompletableFuture[ids.length];
Resource<HttpClient> 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<byte[]> 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<SearchResult, Icon> getThumbnails(List<SearchResult> keys) throws Exception {
int[] ids = keys.stream().mapToInt(SearchResult::getId).toArray();
byte[][] thumbnails = getThumbnails(ids);
Map<SearchResult, Icon> 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;
}
}

View File

@ -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<SearchResult, Icon> getThumbnails(List<SearchResult> 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<SearchResult, Icon> getThumbnails(List<SearchResult> keys) throws Exception {
return ThumbnailServices.TheTVDB.getThumbnails(keys);
}
}
public static class AnidbClientWithLocalSearch extends AnidbClient {

View File

@ -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<T> extends JDialog {
private JList<T> list;
private String command = null;
private Map<T, Icon> icons;
private Function<T, Icon> icon;
public SelectDialog(Component parent, Collection<? extends T> options) {
this(parent, options, null, false, false, null);
}
public SelectDialog(Component parent, Collection<? extends T> options, Map<T, Icon> icons, boolean autoRepeatEnabled, boolean autoRepeatSelected, JComponent header) {
public SelectDialog(Component parent, Collection<? extends T> options, Function<T, Icon> 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<T> extends JDialog {
render.setToolTipText(null);
}
if (icons != null) {
render.setIcon(icons.get(value));
if (icon != null) {
render.setIcon(icon.apply((T) value));
}
}

View File

@ -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

View File

@ -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<SearchResult, Icon> thumbnails = getThumbnails(options);
Function<SearchResult, Icon> thumbnail = thumbnail(options);
// show selection dialog on EDT
Callable<SearchResult> 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<SearchResult> selectDialog = new SelectDialog<SearchResult>(parent, options, thumbnails, true, false, header.getText().isEmpty() ? null : header);
SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(parent, options, thumbnail, true, false, header.getText().isEmpty() ? null : header);
selectDialog.setTitle(provider.getName());
selectDialog.getMessageLabel().setText("<html>Select best match for \"<b>" + escapeHTML(query) + "</b>\":</html>");
selectDialog.getCancelAction().putValue(Action.NAME, "Skip");
@ -293,23 +291,12 @@ class EpisodeListMatcher implements AutoCompleteMatcher {
}
}
protected Map<SearchResult, Icon> getThumbnails(List<SearchResult> options) {
if (provider instanceof TheTVDBClient) {
protected Function<SearchResult, Icon> thumbnail(List<SearchResult> options) {
if (provider instanceof ThumbnailProvider) {
try {
int[] ids = options.stream().mapToInt(SearchResult::getId).toArray();
byte[][] thumbnails = ThumbnailProvider.TheTVDB.getThumbnails(ids);
Map<SearchResult, Icon> 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<SearchResult, Icon> 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);

View File

@ -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<Movie> options, boolean strict, Locale locale, boolean autodetect, Component parent) throws Exception {
protected Movie grabMovieName(File movieFile, List<Movie> 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<Movie> options, Component parent) throws Exception {
protected Movie selectMovie(File movieFile, boolean strict, String userQuery, List<Movie> 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<Movie, Icon> thumbnails = null;
Function<Movie, Icon> thumbnail = thumbnail(options);
// show selection dialog on EDT
Callable<Movie> 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<Movie> selectDialog = new SelectDialog<Movie>(parent, options, thumbnails, true, false, header);
SelectDialog<Movie> selectDialog = new SelectDialog<Movie>(parent, options, thumbnail, true, false, header);
selectDialog.setTitle(service.getName());
selectDialog.getMessageLabel().setText("<html>Select best match for \"<b>" + escapeHTML(query) + "</b>\":</html>");
@ -404,6 +407,20 @@ class MovieMatcher implements AutoCompleteMatcher {
First, Skip;
}
protected Function<Movie, Icon> thumbnail(List<Movie> options) {
if (service instanceof ThumbnailProvider) {
try {
Map<SearchResult, Icon> 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<Match<File, ?>> justFetchMovieInfo(Locale locale, Component parent) throws Exception {
// require user input
String input = showInputDialog("Enter movie name:", "", "Fetch Movie Info", parent);

View File

@ -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<Integer> 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<Set<Integer>> index = Resource.lazy(this::getIndex);
public byte[][] getThumbnails(int[] ids) throws Exception {
synchronized (index) {
CompletableFuture<HttpResponse<byte[]>>[] 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<HttpClient> 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<byte[]> 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<SearchResult, Icon> getThumbnails(List<SearchResult> keys) throws Exception;
}