From 910223641cfaf02858c49cef893ffe46188e7c8e Mon Sep 17 00:00:00 2001 From: Eradev Date: Sat, 25 Jul 2026 04:03:08 -0400 Subject: [PATCH] Add an option to use an alternate API for image fetching --- .../home/settings/CSubmenuPreferences.java | 1 + .../home/settings/VSubmenuPreferences.java | 9 ++++++++ .../java/forge/util/SwingImageFetcher.java | 4 ++-- .../forge/adventure/scene/SettingsScene.java | 1 + .../forge/screens/settings/SettingsPage.java | 3 +++ .../src/forge/util/LibGDXImageFetcher.java | 6 ++--- forge-gui/res/languages/en-US.properties | 2 ++ .../GuiDownloadFilteredCardImages.java | 3 ++- .../gui/download/GuiDownloadPicturesHQ.java | 3 ++- .../properties/ForgeConstants.java | 3 +++ .../properties/ForgePreferences.java | 1 + .../main/java/forge/util/ImageFetcher.java | 22 ++++++++++++++----- 12 files changed, 46 insertions(+), 12 deletions(-) diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java index 7d1cab93ec33..3d0f444c08ee 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java @@ -130,6 +130,7 @@ public void initialize() { lstControls.add(Pair.of(view.getCbEnableNonLegalCards(), FPref.UI_LOAD_NONLEGAL_CARDS)); lstControls.add(Pair.of(view.getCbAllowCustomCardsDeckConformance(), FPref.ALLOW_CUSTOM_CARDS_IN_DECKS_CONFORMANCE)); lstControls.add(Pair.of(view.getCbImageFetcher(), FPref.UI_ENABLE_ONLINE_IMAGE_FETCHER)); + lstControls.add(Pair.of(view.getCbUseAlternateArtApi(), FPref.UI_USE_ALTERNATE_CARD_ART_API)); lstControls.add(Pair.of(view.getCbDisableCardImages(), FPref.UI_DISABLE_CARD_IMAGES)); lstControls.add(Pair.of(view.getCbDisplayFoil(), FPref.UI_OVERLAY_FOIL_EFFECT)); lstControls.add(Pair.of(view.getCbRandomFoil(), FPref.UI_RANDOM_FOIL)); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java index 2334d3dec274..83b665353782 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java @@ -86,6 +86,7 @@ public enum VSubmenuPreferences implements IVSubmenu { private final JCheckBox cbSROptimize = new OptionsCheckBox(localizer.getMessage("cbSROptimize")); private final JCheckBox cbFilteredHands = new OptionsCheckBox(localizer.getMessage("cbFilteredHands")); private final JCheckBox cbImageFetcher = new OptionsCheckBox(localizer.getMessage("cbImageFetcher")); + private final JCheckBox cbUseAlternateArtApi = new OptionsCheckBox(localizer.getMessage("cbUseAlternateArtApi")); private final JCheckBox cbDisableCardImages = new OptionsCheckBox(localizer.getMessage("lblDisableCardImages")); private final JCheckBox cbCloneImgSource = new OptionsCheckBox(localizer.getMessage("cbCloneImgSource")); private final JCheckBox cbScaleLarger = new OptionsCheckBox(localizer.getMessage("cbScaleLarger")); @@ -396,6 +397,9 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(cbImageFetcher, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlImageFetcher")), descriptionConstraints); + pnlPrefs.add(cbUseAlternateArtApi, titleConstraints); + pnlPrefs.add(new NoteLabel(localizer.getMessage("nlUseAlternateArtApi")), descriptionConstraints); + pnlPrefs.add(cbDisableCardImages, titleConstraints); pnlPrefs.add(new NoteLabel(localizer.getMessage("nlDisableCardImages")), descriptionConstraints); @@ -782,6 +786,11 @@ public JCheckBox getCbImageFetcher() { return cbImageFetcher; } + /** @return {@link javax.swing.JCheckBox} */ + public JCheckBox getCbUseAlternateArtApi() { + return cbUseAlternateArtApi; + } + /** @return {@link javax.swing.JCheckBox} */ public JCheckBox getCbDisableCardImages() { return cbDisableCardImages; diff --git a/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java b/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java index cccdb4736f91..84d9f299f664 100644 --- a/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java +++ b/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java @@ -34,9 +34,9 @@ private boolean doFetch(String urlToDownload) throws IOException { return false; } - String newdespath = urlToDownload.contains(".fullborder.jpg") || urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) ? + String newdespath = urlToDownload.contains(".fullborder.jpg") || urlToDownload.startsWith(getCardArtApiBaseUrl()) ? TextUtil.fastReplace(destPath, ".full.jpg", ".fullborder.jpg") : destPath; - if (!newdespath.contains(".full") && !newdespath.contains(".artcrop") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) && !destPath.startsWith(ForgeConstants.CACHE_TOKEN_PICS_DIR)) + if (!newdespath.contains(".full") && !newdespath.contains(".artcrop") && urlToDownload.startsWith(getCardArtApiBaseUrl()) && !destPath.startsWith(ForgeConstants.CACHE_TOKEN_PICS_DIR)) newdespath = newdespath.replace(".jpg", ".fullborder.jpg"); //fix planes/phenomenon for round border options URL url = new URL(urlToDownload); System.out.println("Attempting to fetch: " + url); diff --git a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java index ed3e7f5b9d1d..45a1f26be6ef 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java @@ -313,6 +313,7 @@ public void changed(ChangeEvent event, Actor actor) { addCheckBox(Forge.getLocalizer().getMessage("lblCardID"), ForgePreferences.FPref.UI_OVERLAY_CARD_ID); addCheckBox(Forge.getLocalizer().getMessage("lblAbilityIcon"), ForgePreferences.FPref.UI_OVERLAY_ABILITY_ICONS); addCheckBox(Forge.getLocalizer().getMessage("cbImageFetcher"), ForgePreferences.FPref.UI_ENABLE_ONLINE_IMAGE_FETCHER); + addCheckBox(Forge.getLocalizer().getMessage("cbUseAlternateArtApi"), ForgePreferences.FPref.UI_USE_ALTERNATE_CARD_ART_API); if (!GuiBase.isAndroid()) { addCheckBox(Forge.getLocalizer().getMessage("lblBattlefieldTextureFiltering"), ForgePreferences.FPref.UI_LIBGDX_TEXTURE_FILTERING); diff --git a/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java b/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java index bfc320618272..c9433e5febe4 100644 --- a/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java +++ b/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java @@ -506,6 +506,9 @@ public void select() { lstSettings.addItem(new BooleanSetting(FPref.UI_ENABLE_ONLINE_IMAGE_FETCHER, Forge.getLocalizer().getMessage("cbImageFetcher"), Forge.getLocalizer().getMessage("nlImageFetcher")), 4); + lstSettings.addItem(new BooleanSetting(FPref.UI_USE_ALTERNATE_CARD_ART_API, + Forge.getLocalizer().getMessage("cbUseAlternateArtApi"), + Forge.getLocalizer().getMessage("nlUseAlternateArtApi")), 4); lstSettings.addItem(new CustomSelectSetting(FPref.UI_PREFERRED_ART, Forge.getLocalizer().getMessage("lblPreferredArt"), Forge.getLocalizer().getMessage("nlPreferredArt"), diff --git a/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java b/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java index 18377f202524..c51ce19323cf 100644 --- a/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java +++ b/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java @@ -56,7 +56,7 @@ private boolean doFetch(String urlToDownload) throws IOException { return false; } - if (scryfallCooldownTime != null && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD)) { + if (scryfallCooldownTime != null && urlToDownload.startsWith(getCardArtApiBaseUrl())) { // Don't try to download card images from scryfall if we've been rate limited if (scryfallCooldownTime.after(new Date())) { System.err.println("Currently in cooldown period for scryfall downloads. Skipping download attempt for: " + urlToDownload); @@ -67,9 +67,9 @@ private boolean doFetch(String urlToDownload) throws IOException { } } - String newdespath = urlToDownload.contains(".fullborder.") || urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) ? + String newdespath = urlToDownload.contains(".fullborder.") || urlToDownload.startsWith(getCardArtApiBaseUrl()) ? TextUtil.fastReplace(destPath, ".full.", ".fullborder.") : destPath; - if (!newdespath.contains(".full") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) && + if (!newdespath.contains(".full") && urlToDownload.startsWith(getCardArtApiBaseUrl()) && !destPath.startsWith(ForgeConstants.CACHE_TOKEN_PICS_DIR) && !destPath.startsWith(ForgeConstants.CACHE_PLANECHASE_PICS_DIR)) newdespath = newdespath.replace(".jpg", ".fullborder.jpg"); //fix planes/phenomenon for round border options URL url = new URL(urlToDownload); diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 649196640ae8..1da3e69206b0 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -105,6 +105,7 @@ cbPerformanceMode=Performance Mode cbOrderHand=Keep Hand ordered cbFilteredHands=Filtered Hands cbImageFetcher=Automatically Download Missing Card Art +cbUseAlternateArtApi=Use Alternate Card Art API cbCloneImgSource=Clones Use Original Card Art cbScaleLarger=Scale Image Larger cbRenderBlackCardBorders=Render Black Card Borders @@ -241,6 +242,7 @@ nlCardArtFormat=The format of card art images. (Full: image of entire card. Crop nlDefaultFontSize=The default font size within the UI. All font elements are scaled relative to this. (REQUIRES RESTART) cbpMulliganRule=Mulligan Rule nlImageFetcher=Enables live fetching of missing card images from an online resource. +nlUseAlternateArtApi=Download card images from the alternate API (card-forge.net) instead of Scryfall. nlDisplayFoil=Displays foil cards with the visual foil overlay effect. nlRandomFoil=Adds foil effect to random cards. nlScaleLarger=Allows card pictures to be expanded larger than their original size. diff --git a/forge-gui/src/main/java/forge/gui/download/GuiDownloadFilteredCardImages.java b/forge-gui/src/main/java/forge/gui/download/GuiDownloadFilteredCardImages.java index 0ba812f9948a..797a5fd860ff 100644 --- a/forge-gui/src/main/java/forge/gui/download/GuiDownloadFilteredCardImages.java +++ b/forge-gui/src/main/java/forge/gui/download/GuiDownloadFilteredCardImages.java @@ -7,6 +7,7 @@ import forge.item.PaperCard; import forge.localinstance.properties.ForgeConstants; import forge.model.FModel; +import forge.util.ImageFetcher; import forge.util.ImageUtil; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -97,7 +98,7 @@ private static String buildUrl(PaperCard c, String face) { String langCode = edition.getCardsLangCode(); String path = ImageUtil.getScryfallDownloadUrl(c, face, scryfallCode, langCode, false); if (path != null) { - return ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + path; + return ImageFetcher.buildCardArtApiUrl(path); } } } diff --git a/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java b/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java index c5816dce7028..0c8bf9b305db 100644 --- a/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java +++ b/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java @@ -21,6 +21,7 @@ import forge.item.PaperCard; import forge.localinstance.properties.ForgeConstants; import forge.model.FModel; +import forge.util.ImageFetcher; import forge.util.ImageUtil; import java.io.File; @@ -93,7 +94,7 @@ private void addDLObject(final PaperCard c, final String face) { cardname = cardname.replace(" ", "+"); cardname = cardname.replace("'", ""); - String scryfallurl = ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + "named?fuzzy=" + cardname; + String scryfallurl = ImageFetcher.getCardArtApiBaseUrl() + "named?fuzzy=" + cardname; if(!setCode.equals(CardEdition.UNKNOWN_CODE)) scryfallurl += "&set=" + setCode.toLowerCase(); if(face.equals("back")) scryfallurl += "&face=back"; scryfallurl += "&format=image"; diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java index 9178328faa15..cfdff19943a8 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java @@ -346,6 +346,9 @@ public final class ForgeConstants { public static final String URL_PRICE_DOWNLOAD = GITHUB_ASSETS_BASE + "all-prices.txt"; private static final String URL_SCRYFALL = "https://api.scryfall.com"; public static final String URL_PIC_SCRYFALL_DOWNLOAD = URL_SCRYFALL + "/cards/"; + // Alternate card image API (mirrors Scryfall data, always serves images) + private static final String URL_CARDFORGE_NET_API = "https://card-forge.net/api/v1"; + public static final String URL_PIC_CARDFORGE_NET_API_DOWNLOAD = URL_CARDFORGE_NET_API + "/cards/"; // Constants for Display Card Identity game setting public static final String DISP_CURRENT_COLORS_ALWAYS = "Always"; diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java index fda45bdf6b65..80bf5971afc7 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java @@ -77,6 +77,7 @@ public enum FPref implements AbstractPreferences.IPref { UI_OVERLAY_ABILITY_ICONS("true"), UI_OVERLAY_DRAFT_RANKING("true"), UI_ENABLE_ONLINE_IMAGE_FETCHER ("true"), + UI_USE_ALTERNATE_CARD_ART_API ("false"), UI_PREFERRED_ART("LATEST_ART_ALL_EDITIONS"), UI_SMART_CARD_ART("false"), UI_AUTO_AIDECK_SELECTION("true"), diff --git a/forge-gui/src/main/java/forge/util/ImageFetcher.java b/forge-gui/src/main/java/forge/util/ImageFetcher.java index ff70abbed10b..20f44ba283e6 100644 --- a/forge-gui/src/main/java/forge/util/ImageFetcher.java +++ b/forge-gui/src/main/java/forge/util/ImageFetcher.java @@ -39,6 +39,18 @@ public abstract class ImageFetcher { private HashMap> currentFetches = new HashMap<>(); + /** @return the base "/cards/" URL of the currently selected card image API. */ + public static String getCardArtApiBaseUrl() { + return FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.UI_USE_ALTERNATE_CARD_ART_API) + ? ForgeConstants.URL_PIC_CARDFORGE_NET_API_DOWNLOAD + : ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD; + } + + /** Builds a full download URL from a path produced by {@link ImageUtil#getScryfallDownloadUrl}. */ + public static String buildCardArtApiUrl(String path) { + return getCardArtApiBaseUrl() + path; + } + private String getScryfallDownloadURL(PaperCard c, String face, boolean useArtCrop, boolean hasSetLookup, String imagePath, ArrayList downloadUrls) { StaticData data = StaticData.instance(); CardEdition edition = data.getEditions().get(c.getEdition()); @@ -64,7 +76,7 @@ private String getScryfallDownloadURL(PaperCard c, String face, boolean useArtCr } else { addScryfallUrl(c, face, useArtCrop, downloadUrls); String setCode = edition.getScryfallCode(); - downloadUrls.add(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + ImageUtil.getScryfallDownloadUrl(c, face, setCode, "", useArtCrop)); + downloadUrls.add(buildCardArtApiUrl(ImageUtil.getScryfallDownloadUrl(c, face, setCode, "", useArtCrop))); } return null; } @@ -77,7 +89,7 @@ private void addScryfallUrl(PaperCard card, String face, boolean useArtCrop, Arr String setCode = edition.getScryfallCode(); String langCode = edition.getCardsLangCode(); - String primaryUrl = ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + ImageUtil.getScryfallDownloadUrl(card, face, setCode, langCode, useArtCrop); + String primaryUrl = buildCardArtApiUrl(ImageUtil.getScryfallDownloadUrl(card, face, setCode, langCode, useArtCrop)); if (!downloadUrls.contains(primaryUrl)) { downloadUrls.add(primaryUrl); } @@ -151,7 +163,7 @@ public void fetchImage(final String imageKey, final Callback callback) { if (ed != null) { String setCode = ed.getScryfallCode(); String langCode = ed.getCardsLangCode(); - downloadUrls.add("PLANECHASEBG:" + ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + ImageUtil.getScryfallDownloadUrl(pc, "", setCode, langCode, true)); + downloadUrls.add("PLANECHASEBG:" + buildCardArtApiUrl(ImageUtil.getScryfallDownloadUrl(pc, "", setCode, langCode, true))); FileUtil.ensureDirectoryExists(ForgeConstants.CACHE_PLANECHASE_PICS_DIR); File destFile = new File(ForgeConstants.CACHE_PLANECHASE_PICS_DIR, getPlanechaseFilename(cardName)); if (destFile.exists()) @@ -327,7 +339,7 @@ public void fetchImage(final String imageKey, final Callback callback) { String tokenCode = edition.getTokensCode(); String langCode = edition.getCardsLangCode(); // Just assume the CNr from the token image is valid - downloadUrls.add(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + ImageUtil.getScryfallTokenDownloadUrl(tempdata[2], tokenCode, langCode, face)); + downloadUrls.add(buildCardArtApiUrl(ImageUtil.getScryfallTokenDownloadUrl(tempdata[2], tokenCode, langCode, face))); } else if (!allTokens.isEmpty()) { // This loop is going to try to download all the arts until it finds one // This is a bit wrong since it _should_ just be trying to get the one with the appropriate collector number @@ -345,7 +357,7 @@ public void fetchImage(final String imageKey, final Callback callback) { continue; } - downloadUrls.add(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + ImageUtil.getScryfallTokenDownloadUrl(tis.collectorNumber(), tokenCode, langCode, face)); + downloadUrls.add(buildCardArtApiUrl(ImageUtil.getScryfallTokenDownloadUrl(tis.collectorNumber(), tokenCode, langCode, face))); } }