Merge branch 'patch-imagefetcher-scryfall' into 'master'

FIX ImageFetcher against Scryfall, so that download can be robust (against different set-codes) and reliable.

See merge request core-developers/forge!4788
This commit is contained in:
Michael Kamensky
2021-05-29 06:54:06 +00:00
2 changed files with 100 additions and 30 deletions

View File

@@ -118,6 +118,27 @@ public class ImageUtil {
return getImageRelativePath(cp, backFace, true, true); return getImageRelativePath(cp, backFace, true, true);
} }
public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode){
return getScryfallDownloadUrl(cp, backFace, setCode, "en");
}
public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode, String langCode){
String editionCode;
if ((setCode != null) && (setCode.length() > 0))
editionCode = setCode;
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.replace("+", "");
String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (backFace ? "&face=back" : "&face=front");
}
return String.format("%s/%s/%s?format=image&version=normal%s", editionCode, cardCollectorNumber,
langCode, faceParam);
}
public static String toMWSFilename(String in) { public static String toMWSFilename(String in) {
final StringBuilder out = new StringBuilder(); final StringBuilder out = new StringBuilder();
char c; char c;

View File

@@ -1,14 +1,14 @@
package forge.util; package forge.util;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.io.IOException;
import java.util.HashMap; import java.net.HttpURLConnection;
import java.util.HashSet; import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import forge.card.CardEdition;
import forge.item.IPaperCard; import forge.item.IPaperCard;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -25,6 +25,9 @@ public abstract class ImageFetcher {
// see https://scryfall.com/docs/api/languages and // see https://scryfall.com/docs/api/languages and
// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes // https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
private static final HashMap<String, String> langCodeMap = new HashMap<>(); private static final HashMap<String, String> langCodeMap = new HashMap<>();
private static final Map<String, String> scryfallSetCodes = new HashMap<>();
private static final String INVALID_SCRYFALL_SET_CODE = "NotFound";
static { static {
langCodeMap.put("en-US", "en"); langCodeMap.put("en-US", "en");
langCodeMap.put("es-ES", "es"); langCodeMap.put("es-ES", "es");
@@ -41,6 +44,65 @@ public abstract class ImageFetcher {
private HashMap<String, HashSet<Callback>> currentFetches = new HashMap<>(); private HashMap<String, HashSet<Callback>> currentFetches = new HashMap<>();
private HashMap<String, String> tokenImages; private HashMap<String, String> tokenImages;
private static boolean isValidScryfallURL(final String urlString){
try {
URL u = new URL(urlString);
HttpURLConnection huc = (HttpURLConnection) u.openConnection();
huc.setInstanceFollowRedirects(true);
huc.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.2) " +
"Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729)");
huc.setRequestMethod("HEAD");
return (huc.getResponseCode() == HttpURLConnection.HTTP_OK);
} catch (IOException e) {
return false;
}
}
private String getScryfallDownloadURL(PaperCard c, boolean backFace, String langCode){
String setCode = scryfallSetCodes.getOrDefault(c.getEdition(), null);
if ((setCode != null) && (!setCode.equals(INVALID_SCRYFALL_SET_CODE))){
return ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD +
ImageUtil.getScryfallDownloadUrl(c, backFace, setCode, langCode);
}
// No entry matched yet for edition
StaticData data = StaticData.instance();
CardEdition edition = data.getEditions().get(c.getEdition());
if (edition == null) // edition does not exist - some error occurred with card data
return null;
// 1. Try MCI code first, as it original.
String mciCode = edition.getMciCode().toLowerCase();
String url = ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD +
ImageUtil.getScryfallDownloadUrl(c, backFace, mciCode, langCode);
if (isValidScryfallURL(url)) {
scryfallSetCodes.put(c.getEdition(), setCode);
return url;
}
// 2. MCI didn't work, so now try all other codes available in edition, alias included.
// skipping dups with set, and returning as soon as one will work.
Set<String> cardSetCodes = new HashSet<>();
// all set-codes should be lower case
cardSetCodes.add(mciCode); // add MCI
cardSetCodes.add(edition.getCode().toLowerCase());
cardSetCodes.add(edition.getCode2().toLowerCase());
if (edition.getAlias() != null)
cardSetCodes.add(edition.getAlias().toLowerCase());
for (String code : cardSetCodes) {
if (code.equals(mciCode))
continue; // Already checked, SKIP
url = ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD +
ImageUtil.getScryfallDownloadUrl(c, backFace, code, langCode);
if (isValidScryfallURL(url)) {
scryfallSetCodes.put(c.getEdition(), setCode);
return url;
}
}
// If we're here, no valid URL has been found. Record this for the future
scryfallSetCodes.put(c.getEdition(), INVALID_SCRYFALL_SET_CODE);
return null;
}
public void fetchImage(final String imageKey, final Callback callback) { public void fetchImage(final String imageKey, final Callback callback) {
FThreads.assertExecutedByEdt(true); FThreads.assertExecutedByEdt(true);
@@ -63,38 +125,24 @@ public abstract class ImageFetcher {
System.err.println("Paper card not found for: " + imageKey); System.err.println("Paper card not found for: " + imageKey);
return; return;
} }
final boolean backFace = imageKey.endsWith(ImageKeys.BACKFACE_POSTFIX); final boolean backFace = ImageUtil.hasBackFacePicture(paperCard);
final String filename = ImageUtil.getImageKey(paperCard, backFace, true); final String filename = ImageUtil.getImageKey(paperCard, backFace, true);
destFile = new File(ForgeConstants.CACHE_CARD_PICS_DIR + "/" + filename + ".jpg"); destFile = new File(ForgeConstants.CACHE_CARD_PICS_DIR, filename + ".jpg");
//move priority of ftp image here //move priority of ftp image here
StringBuilder setDownload = new StringBuilder(ForgeConstants.URL_PIC_DOWNLOAD); StringBuilder setDownload = new StringBuilder(ForgeConstants.URL_PIC_DOWNLOAD);
setDownload.append(ImageUtil.getDownloadUrl(paperCard, backFace)); setDownload.append(ImageUtil.getDownloadUrl(paperCard, backFace));
downloadUrls.add(setDownload.toString()); downloadUrls.add(setDownload.toString());
int artIndex = 1;
final Pattern pattern = Pattern.compile("^.:([^|]*\\|){2}(\\d+).*$");
Matcher matcher = pattern.matcher(imageKey);
if (matcher.matches()) {
artIndex = Integer.parseInt(matcher.group(2));
}
final StaticData data = StaticData.instance();
final String cardCollectorNumber = paperCard.getCollectorNumber(); final String cardCollectorNumber = paperCard.getCollectorNumber();
if (!cardCollectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)){ if (!cardCollectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)){
String faceParam = "";
if (paperCard.getRules().getOtherPart() != null) {
faceParam = (backFace ? "&face=back" : "&face=front");
}
final String editionMciCode = data.getEditions().getMciCodeByCode(paperCard.getEdition());
String langCode = "en"; String langCode = "en";
String UILang = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_LANGUAGE); String UILang = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_LANGUAGE);
if (langCodeMap.containsKey(UILang)) { if (langCodeMap.containsKey(UILang))
langCode = langCodeMap.get(UILang); langCode = langCodeMap.get(UILang);
} final String scryfallURL = this.getScryfallDownloadURL(paperCard, backFace, langCode);
// see https://scryfall.com/blog 2020/8/6, and if (scryfallURL == null)
// https://scryfall.com/docs/api/cards/collector return; // Non existing card, or Card's set not found in Scryfall
downloadUrls.add(String.format("https://api.scryfall.com/cards/%s/%s/%s?format=image&version=normal%s", downloadUrls.add(scryfallURL);
editionMciCode, cardCollectorNumber, langCode, faceParam));
} }
} else if (prefix.equals(ImageKeys.TOKEN_PREFIX)) { } else if (prefix.equals(ImageKeys.TOKEN_PREFIX)) {
@@ -122,10 +170,11 @@ public abstract class ImageFetcher {
} }
if (destFile.exists()) { if (destFile.exists()) {
// TODO: Figure out why this codepath gets reached. Ideally, fetchImage() wouldn't // TODO: Figure out why this codepath gets reached.
// be called if we already have the image. // Ideally, fetchImage() wouldn't be called if we already have the image.
return; return;
} }
final String destPath = destFile.getAbsolutePath(); final String destPath = destFile.getAbsolutePath();
// Note: No synchronization is needed here because this is executed on // Note: No synchronization is needed here because this is executed on