mirror of
https://github.com/mitb-archive/filebot
synced 2024-08-13 17:03:45 -04:00
Experiment with artwork thumbnail support
This commit is contained in:
parent
72f2f68d0e
commit
3db219f1fb
113
source/net/filebot/ThumbnailServices.java
Normal file
113
source/net/filebot/ThumbnailServices.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user