Add option to download missing card art on the fly

This commit is contained in:
Pedro Ferreira
2018-10-30 23:14:28 +01:00
parent a564e6af53
commit 7765ff8b33
15 changed files with 277 additions and 75 deletions

View File

@@ -4,6 +4,9 @@ import java.awt.image.BufferedImage;
import forge.game.card.CardView;
import forge.game.player.PlayerView;
import forge.util.ImageFetcher;
import forge.util.SwingImageFetcher;
public abstract class CachedCardImage implements ImageFetcher.Callback {
final CardView card;
@@ -11,6 +14,8 @@ public abstract class CachedCardImage implements ImageFetcher.Callback {
final int width;
final int height;
static final SwingImageFetcher fetcher = new SwingImageFetcher();
public CachedCardImage(final CardView card, final Iterable<PlayerView> viewers, final int width, final int height) {
this.card = card;
this.viewers = viewers;
@@ -19,7 +24,7 @@ public abstract class CachedCardImage implements ImageFetcher.Callback {
BufferedImage image = ImageCache.getImageNoDefault(card, viewers, width, height);
if (image == null) {
String key = card.getCurrentState().getImageKey(viewers);
ImageFetcher.fetchImage(card, key, this);
fetcher.fetchImage(key, this);
}
}

View File

@@ -23,10 +23,7 @@ import forge.sound.*;
import forge.toolbox.FOptionPane;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinImage;
import forge.util.BuildInfo;
import forge.util.Callback;
import forge.util.FileUtil;
import forge.util.OperatingSystem;
import forge.util.*;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
@@ -42,6 +39,8 @@ import java.util.Collection;
import java.util.List;
public class GuiDesktop implements IGuiBase {
private ImageFetcher imageFetcher = new SwingImageFetcher();
@Override
public boolean isRunningOnDesktop() {
return true;
@@ -63,6 +62,11 @@ public class GuiDesktop implements IGuiBase {
"../forge-gui/" : "";
}
@Override
public ImageFetcher getImageFetcher() {
return imageFetcher;
}
@Override
public void invokeInEdtNow(final Runnable proc) {
proc.run();

View File

@@ -1,168 +0,0 @@
package forge;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.imageio.ImageIO;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.tuple.Pair;
import forge.game.card.CardView;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.properties.ForgeConstants;
import forge.properties.ForgePreferences;
import forge.util.FileUtil;
import forge.util.ImageUtil;
public class ImageFetcher {
private static final ExecutorService threadPool = Executors.newCachedThreadPool();
private static HashMap<String, HashSet<Callback>> currentFetches = new HashMap<>();
private static HashMap<String, String> tokenImages;
public static void fetchImage(final CardView card, final String imageKey, final Callback callback) {
FThreads.assertExecutedByEdt(true);
if (!FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.UI_ENABLE_ONLINE_IMAGE_FETCHER))
return;
// Fake card (like the ante prompt) trying to be "fetched"
if (imageKey.length() < 2)
return;
final String prefix = imageKey.substring(0, 2);
final ArrayList<String> downloadUrls = new ArrayList<>();
File destFile = null;
if (prefix.equals(ImageKeys.CARD_PREFIX)) {
PaperCard paperCard = ImageUtil.getPaperCardFromImageKey(imageKey);
if (paperCard == null) {
System.err.println("Paper card not found for: " + imageKey);
return;
}
final boolean backFace = imageKey.endsWith(ImageKeys.BACKFACE_POSTFIX);
final String filename = ImageUtil.getImageKey(paperCard, backFace, true);
destFile = new File(ForgeConstants.CACHE_CARD_PICS_DIR + "/" + filename + ".jpg");
// First try to download the LQ Set URL, then fetch from scryfall/magiccards.info
StringBuilder setDownload = new StringBuilder(ForgeConstants.URL_PIC_DOWNLOAD);
setDownload.append(ImageUtil.getDownloadUrl(paperCard, backFace));
downloadUrls.add(setDownload.toString());
final StaticData data = StaticData.instance();
final int cardNum = data.getCommonCards().getCardCollectorNumber(paperCard.getName(), paperCard.getEdition());
if (cardNum != -1) {
String suffix = "";
if (paperCard.getRules().getOtherPart() != null) {
suffix = (backFace ? "b" : "a");
}
final String editionMciCode = data.getEditions().getMciCodeByCode(paperCard.getEdition());
downloadUrls.add(String.format("https://img.scryfall.com/cards/normal/en/%s/%d%s.jpg", editionMciCode, cardNum, suffix));
downloadUrls.add(String.format("https://magiccards.info/scans/en/%s/%d%s.jpg", editionMciCode, cardNum, suffix));
}
} else if (prefix.equals(ImageKeys.TOKEN_PREFIX)) {
if (tokenImages == null) {
tokenImages = new HashMap<>();
for (Pair<String, String> nameUrlPair : FileUtil.readNameUrlFile(ForgeConstants.IMAGE_LIST_TOKENS_FILE)) {
tokenImages.put(nameUrlPair.getLeft(), nameUrlPair.getRight());
}
}
final String filename = imageKey.substring(2) + ".jpg";
String tokenUrl = tokenImages.get(filename);
if (tokenUrl == null) {
System.err.println("No specified file.. Attempting to download from default Url");
tokenUrl = String.format("%s%s", ForgeConstants.URL_TOKEN_DOWNLOAD, filename);
}
destFile = new File(ForgeConstants.CACHE_TOKEN_PICS_DIR, filename);
downloadUrls.add(tokenUrl);
}
if (downloadUrls.isEmpty()) {
System.err.println("No download URLs for: " + imageKey);
return;
}
if (destFile.exists()) {
// TODO: Figure out why this codepath gets reached. Ideally, fetchImage() wouldn't
// be called if we already have the image.
return;
}
final String destPath = destFile.getAbsolutePath();
// Note: No synchronization is needed here because this is executed on
// EDT thread (see assert on top) and so is the notification of observers.
HashSet<Callback> observers = currentFetches.get(destPath);
if (observers != null) {
// Already in the queue, simply add the new observer.
observers.add(callback);
return;
}
observers = new HashSet<>();
observers.add(callback);
currentFetches.put(destPath, observers);
final Runnable notifyObservers = new Runnable() {
public void run() {
FThreads.assertExecutedByEdt(true);
for (Callback o : currentFetches.get(destPath)) {
o.onImageFetched();
}
currentFetches.remove(destPath);
}
};
threadPool.submit(new DownloadTask(downloadUrls.toArray(new String[0]), destPath, notifyObservers));
}
public static interface Callback {
public void onImageFetched();
}
private static class DownloadTask implements Runnable {
private final String[] downloadUrls;
private final String destPath;
private final Runnable notifyObservers;
public DownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
this.downloadUrls = downloadUrls;
this.destPath = destPath;
this.notifyObservers = notifyObservers;
}
private void doFetch(String urlToDownload) throws IOException {
URL url = new URL(urlToDownload);
System.out.println("Attempting to fetch: " + url);
java.net.URLConnection c = url.openConnection();
c.setRequestProperty("User-Agent", "");
BufferedImage image = ImageIO.read(c.getInputStream());
// First, save to a temporary file so that nothing tries to read
// a partial download.
File destFile = new File(destPath + ".tmp");
destFile.mkdirs();
ImageIO.write(image, "jpg", destFile);
// Now, rename it to the correct name.
destFile.renameTo(new File(destPath));
System.out.println("Saved image to " + destPath);
SwingUtilities.invokeLater(notifyObservers);
}
public void run() {
for (String urlToDownload : downloadUrls) {
try {
doFetch(urlToDownload);
break;
} catch (IOException e) {
System.out.println("Failed to download card [" + destPath + "] image: " + e.getMessage());
}
}
}
}
}

View File

@@ -24,8 +24,9 @@ import java.awt.image.WritableRaster;
import javax.swing.JPanel;
import forge.GuiBase;
import forge.ImageCache;
import forge.ImageFetcher;
import forge.util.ImageFetcher;
import forge.ImageKeys;
import forge.game.card.CardView.CardStateView;
import forge.item.InventoryItem;
@@ -115,7 +116,7 @@ public final class CardPicturePanel extends JPanel implements ImageFetcher.Callb
CardStateView card = (CardStateView) displayed;
BufferedImage image = ImageCache.getOriginalImage(card.getImageKey(), false);
if (image == null) {
ImageFetcher.fetchImage(card.getCard(), card.getImageKey(), this);
GuiBase.getInterface().getImageFetcher().fetchImage(card.getImageKey(), this);
}
return FImageUtil.getImage((CardStateView) displayed);
}

View File

@@ -0,0 +1,57 @@
package forge.util;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class SwingImageFetcher extends ImageFetcher {
@Override
protected Runnable getDownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
return new SwingDownloadTask(downloadUrls, destPath, notifyObservers);
}
private static class SwingDownloadTask implements Runnable {
private final String[] downloadUrls;
private final String destPath;
private final Runnable notifyObservers;
public SwingDownloadTask(String[] downloadUrls, String destPath, Runnable notifyObservers) {
this.downloadUrls = downloadUrls;
this.destPath = destPath;
this.notifyObservers = notifyObservers;
}
private void doFetch(String urlToDownload) throws IOException {
URL url = new URL(urlToDownload);
System.out.println("Attempting to fetch: " + url);
java.net.URLConnection c = url.openConnection();
c.setRequestProperty("User-Agent", "");
BufferedImage image = ImageIO.read(c.getInputStream());
// First, save to a temporary file so that nothing tries to read
// a partial download.
File destFile = new File(destPath + ".tmp");
destFile.mkdirs();
ImageIO.write(image, "jpg", destFile);
// Now, rename it to the correct name.
destFile.renameTo(new File(destPath));
System.out.println("Saved image to " + destPath);
SwingUtilities.invokeLater(notifyObservers);
}
public void run() {
for (String urlToDownload : downloadUrls) {
try {
doFetch(urlToDownload);
break;
} catch (IOException e) {
System.out.println("Failed to download card [" + destPath + "] image: " + e.getMessage());
}
}
}
}
}