mirror of
https://github.com/magefree/mage.git
synced 2026-01-26 21:29:17 -08:00
Merge 6080969d82 into 162edb9351
This commit is contained in:
commit
0797e1302c
5 changed files with 488 additions and 23 deletions
|
|
@ -84,6 +84,7 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
public static final String KEY_CARD_IMAGES_PATH = "cardImagesPath";
|
||||
public static final String KEY_CARD_IMAGES_SAVE_TO_ZIP = "cardImagesSaveToZip";
|
||||
public static final String KEY_CARD_IMAGES_PREF_LANGUAGE = "cardImagesPreferredImageLaguage";
|
||||
public static final String KEY_CARD_IMAGES_AUTO_DOWNLOAD = "cardImagesAutoDownload";
|
||||
|
||||
public static final String KEY_CARD_RENDERING_IMAGE_MODE = "cardRenderingMode";
|
||||
public static final String KEY_CARD_RENDERING_ICONS_FOR_ABILITIES = "cardRenderingIconsForAbilities";
|
||||
|
|
@ -957,6 +958,7 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
txtImageFolderPath = new javax.swing.JTextField();
|
||||
btnBrowseImageLocation = new javax.swing.JButton();
|
||||
cbSaveToZipFiles = new javax.swing.JCheckBox();
|
||||
cbAutoDownloadImages = new javax.swing.JCheckBox();
|
||||
cbPreferredImageLanguage = new javax.swing.JComboBox<>();
|
||||
labelPreferredImageLanguage = new javax.swing.JLabel();
|
||||
panelCardStyles = new javax.swing.JPanel();
|
||||
|
|
@ -2277,6 +2279,9 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
}
|
||||
});
|
||||
|
||||
cbAutoDownloadImages.setText("Download missing images automatically");
|
||||
cbAutoDownloadImages.setToolTipText("Automatically download card images from Scryfall when they are first displayed");
|
||||
|
||||
cbPreferredImageLanguage.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" }));
|
||||
|
||||
labelPreferredImageLanguage.setText("Default images language:");
|
||||
|
|
@ -2297,6 +2302,7 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
.add(panelCardImagesLayout.createSequentialGroup()
|
||||
.add(panelCardImagesLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
|
||||
.add(cbSaveToZipFiles)
|
||||
.add(cbAutoDownloadImages)
|
||||
.add(panelCardImagesLayout.createSequentialGroup()
|
||||
.add(6, 6, 6)
|
||||
.add(labelPreferredImageLanguage)
|
||||
|
|
@ -2315,6 +2321,8 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
.addPreferredGap(org.jdesktop.layout.LayoutStyle.UNRELATED)
|
||||
.add(cbSaveToZipFiles)
|
||||
.addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
|
||||
.add(cbAutoDownloadImages)
|
||||
.addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
|
||||
.add(panelCardImagesLayout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
|
||||
.add(labelPreferredImageLanguage)
|
||||
.add(cbPreferredImageLanguage, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
|
||||
|
|
@ -3058,6 +3066,7 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
save(prefs, dialog.cbUseDefaultImageFolder, KEY_CARD_IMAGES_USE_DEFAULT, "true", "false");
|
||||
saveImagesPath(prefs);
|
||||
save(prefs, dialog.cbSaveToZipFiles, KEY_CARD_IMAGES_SAVE_TO_ZIP, "true", "false");
|
||||
save(prefs, dialog.cbAutoDownloadImages, KEY_CARD_IMAGES_AUTO_DOWNLOAD, "true", "false");
|
||||
save(prefs, dialog.cbPreferredImageLanguage, KEY_CARD_IMAGES_PREF_LANGUAGE);
|
||||
|
||||
save(prefs, dialog.cbUseDefaultBackground, KEY_BACKGROUND_IMAGE_DEFAULT, "true", "false");
|
||||
|
|
@ -3513,6 +3522,7 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
updateCache(KEY_CARD_IMAGES_PATH, path);
|
||||
}
|
||||
load(prefs, dialog.cbSaveToZipFiles, KEY_CARD_IMAGES_SAVE_TO_ZIP, "true", "false");
|
||||
load(prefs, dialog.cbAutoDownloadImages, KEY_CARD_IMAGES_AUTO_DOWNLOAD, "true", "false");
|
||||
dialog.cbPreferredImageLanguage.setSelectedItem(MageFrame.getPreferences().get(KEY_CARD_IMAGES_PREF_LANGUAGE, CardLanguage.ENGLISH.getCode()));
|
||||
|
||||
// rendering settings
|
||||
|
|
@ -3705,6 +3715,10 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
return PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_SAVE_TO_ZIP, "false").equals("true");
|
||||
}
|
||||
|
||||
public static boolean isAutoDownloadEnabled() {
|
||||
return PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_AUTO_DOWNLOAD, "false").equals("true");
|
||||
}
|
||||
|
||||
private static void load(Preferences prefs, JCheckBox checkBox, String propName, String yesValue) {
|
||||
String prop = prefs.get(propName, yesValue);
|
||||
checkBox.setSelected(prop.equals(yesValue));
|
||||
|
|
@ -4066,6 +4080,7 @@ public class PreferencesDialog extends javax.swing.JDialog {
|
|||
private javax.swing.JButton buttonSizeDefault6;
|
||||
private javax.swing.JCheckBox cbAllowRequestToShowHandCards;
|
||||
private javax.swing.JCheckBox cbAskMoveToGraveOrder;
|
||||
private javax.swing.JCheckBox cbAutoDownloadImages;
|
||||
private javax.swing.JCheckBox cbAutoOrderTrigger;
|
||||
private javax.swing.JCheckBox cbCardRenderHideSetSymbol;
|
||||
private javax.swing.JCheckBox cbCardRenderIconsForAbilities;
|
||||
|
|
|
|||
|
|
@ -15,9 +15,12 @@ import mage.constants.SubType;
|
|||
import mage.util.DebugUtil;
|
||||
import mage.view.CardView;
|
||||
import mage.view.CounterView;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.jdesktop.swingx.graphics.GraphicsUtilities;
|
||||
import org.mage.plugins.card.images.CardDownloadData;
|
||||
import org.mage.plugins.card.images.ImageCache;
|
||||
import org.mage.plugins.card.images.ImageCacheData;
|
||||
import org.mage.plugins.card.images.OnDemandImageDownloader;
|
||||
import org.mage.plugins.card.utils.impl.ImageManagerImpl;
|
||||
|
||||
import javax.swing.*;
|
||||
|
|
@ -26,6 +29,7 @@ import java.awt.image.BufferedImage;
|
|||
import java.util.Objects;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Render mode: IMAGE
|
||||
|
|
@ -34,6 +38,8 @@ import java.util.UUID;
|
|||
*/
|
||||
public class CardPanelRenderModeImage extends CardPanel {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(CardPanelRenderModeImage.class);
|
||||
|
||||
private static final long serialVersionUID = -3272134219262184411L;
|
||||
|
||||
private static final SoftValuesLoadingCache<Key, BufferedImage> IMAGE_MODE_RENDERED_CACHE = ImageCaches.register(SoftValuesLoadingCache.from(CardPanelRenderModeImage::createImage));
|
||||
|
|
@ -82,6 +88,9 @@ public class CardPanelRenderModeImage extends CardPanel {
|
|||
|
||||
private int updateArtImageStamp;
|
||||
|
||||
// Listener for on-demand image downloads
|
||||
private Consumer<CardDownloadData> downloadListener;
|
||||
|
||||
private static class Key {
|
||||
|
||||
final Insets border;
|
||||
|
|
@ -356,6 +365,13 @@ public class CardPanelRenderModeImage extends CardPanel {
|
|||
public void cleanUp() {
|
||||
super.cleanUp();
|
||||
counterPanel = null;
|
||||
|
||||
// Unregister download listener
|
||||
if (downloadListener != null) {
|
||||
LOGGER.debug("cleanUp: removing download listener");
|
||||
OnDemandImageDownloader.getInstance().removeDownloadListener(downloadListener);
|
||||
downloadListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -641,13 +657,25 @@ public class CardPanelRenderModeImage extends CardPanel {
|
|||
|
||||
final int stamp = ++updateArtImageStamp;
|
||||
|
||||
// Capture card info on EDT before submitting to thread pool
|
||||
final CardView card = getGameCard();
|
||||
if (card == null) {
|
||||
return;
|
||||
}
|
||||
final String cardName = card.getName();
|
||||
final String setCode = card.getExpansionSetCode();
|
||||
final String cardNumber = card.getCardNumber();
|
||||
|
||||
Util.threadPool.submit(() -> {
|
||||
try {
|
||||
ImageCacheData data = ImageCache.getCardImage(getGameCard(), getCardWidth(), getCardHeight());
|
||||
ImageCacheData data = ImageCache.getCardImage(card, getCardWidth(), getCardHeight());
|
||||
|
||||
// save missing image
|
||||
LOGGER.debug("updateArtImage for " + cardName + " [" + setCode + "/" + cardNumber + "] image=" + (data.getImage() != null ? "found" : "NULL"));
|
||||
if (data.getImage() == null) {
|
||||
setFullPath(data.getPath());
|
||||
// Register for download notification if not already registered
|
||||
registerDownloadListener(cardName, setCode, cardNumber);
|
||||
}
|
||||
|
||||
UI.invokeLater(() -> {
|
||||
|
|
@ -655,6 +683,12 @@ public class CardPanelRenderModeImage extends CardPanel {
|
|||
hasImage = data.getImage() != null;
|
||||
setTitle(getGameCard());
|
||||
setImage(data.getImage());
|
||||
|
||||
// Unregister listener if we now have an image
|
||||
if (hasImage && downloadListener != null) {
|
||||
OnDemandImageDownloader.getInstance().removeDownloadListener(downloadListener);
|
||||
downloadListener = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception | Error e) {
|
||||
|
|
@ -663,6 +697,39 @@ public class CardPanelRenderModeImage extends CardPanel {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener to refresh this card when its image is downloaded.
|
||||
*/
|
||||
private void registerDownloadListener(String cardName, String setCode, String cardNumber) {
|
||||
// Only register if auto-download is enabled
|
||||
if (!PreferencesDialog.isAutoDownloadEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloadListener != null) {
|
||||
return; // Already registered
|
||||
}
|
||||
|
||||
LOGGER.debug("Registering download listener for: " + cardName + " [" + setCode + "/" + cardNumber + "]");
|
||||
|
||||
downloadListener = downloadedCard -> {
|
||||
// Check if this download matches our card
|
||||
boolean matches = downloadedCard.getName().equals(cardName)
|
||||
&& downloadedCard.getSet().equals(setCode)
|
||||
&& downloadedCard.getCollectorId().equals(cardNumber);
|
||||
|
||||
LOGGER.debug("Listener received: " + downloadedCard.getName() + " [" + downloadedCard.getSet() + "/" + downloadedCard.getCollectorId() + "] matches=" + matches + " (looking for " + cardName + " [" + setCode + "/" + cardNumber + "])");
|
||||
|
||||
if (matches) {
|
||||
// Refresh this card's image on the EDT
|
||||
LOGGER.debug("Triggering updateArtImage for: " + cardName);
|
||||
UI.invokeLater(this::updateArtImage);
|
||||
}
|
||||
};
|
||||
|
||||
OnDemandImageDownloader.getInstance().addDownloadListener(downloadListener);
|
||||
}
|
||||
|
||||
private int getManaWidth(String manaCost, int symbolMarginX) {
|
||||
int width = 0;
|
||||
manaCost = manaCost.replace("\\", "");
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package org.mage.card.arcane;
|
|||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import mage.cards.action.ActionCallback;
|
||||
import mage.client.dialog.PreferencesDialog;
|
||||
import mage.client.util.ImageCaches;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
|
|
@ -10,19 +11,25 @@ import mage.constants.SuperType;
|
|||
import mage.view.CardView;
|
||||
import mage.view.CounterView;
|
||||
import mage.view.PermanentView;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.jdesktop.swingx.graphics.GraphicsUtilities;
|
||||
import org.mage.plugins.card.images.CardDownloadData;
|
||||
import org.mage.plugins.card.images.ImageCache;
|
||||
import org.mage.plugins.card.images.OnDemandImageDownloader;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Render mode: MTGO
|
||||
*/
|
||||
public class CardPanelRenderModeMTGO extends CardPanel {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(CardPanelRenderModeMTGO.class);
|
||||
|
||||
// TODO: share code and use for all images/rendering (potential injection point - images cache), see #969
|
||||
private static final boolean MTGO_MODE_RENDER_SMOOTH_IMAGES_ENABLED = false;
|
||||
private static final int MTGO_MODE_RENDER_SCALED_IMAGES_COEF = 1; // TODO: experiment with scale settings, is it useful to render in x2-x4 sizes?
|
||||
|
|
@ -52,6 +59,9 @@ public class CardPanelRenderModeMTGO extends CardPanel {
|
|||
private int updateArtImageStamp;
|
||||
private final int cardRenderMode;
|
||||
|
||||
// Listener for on-demand image downloads
|
||||
private Consumer<CardDownloadData> downloadListener;
|
||||
|
||||
private static class ImageKey {
|
||||
final BufferedImage artImage;
|
||||
final int width;
|
||||
|
|
@ -288,32 +298,89 @@ public class CardPanelRenderModeMTGO extends CardPanel {
|
|||
// Schedule a repaint
|
||||
repaint();
|
||||
|
||||
// See if the image is already loaded
|
||||
//artImage = ImageCache.tryGetImage(gameCard, getCardWidth(), getCardHeight());
|
||||
//this.cardRenderer.setArtImage(artImage);
|
||||
// Capture card info on EDT before submitting to thread pool
|
||||
final CardView card = getGameCard();
|
||||
if (card == null) {
|
||||
return;
|
||||
}
|
||||
final String cardName = card.getName();
|
||||
final String setCode = card.getExpansionSetCode();
|
||||
final String cardNumber = card.getCardNumber();
|
||||
|
||||
// Submit a task to draw with the card art when it arrives
|
||||
if (artImage == null) {
|
||||
final int stamp = ++updateArtImageStamp;
|
||||
Util.threadPool.submit(() -> {
|
||||
try {
|
||||
final BufferedImage srcImage;
|
||||
srcImage = ImageCache.getCardImage(getGameCard(), getCardWidth(), getCardHeight()).getImage();
|
||||
UI.invokeLater(() -> {
|
||||
if (stamp == updateArtImageStamp) {
|
||||
artImage = srcImage;
|
||||
cardRenderer.setArtImage(srcImage);
|
||||
if (srcImage != null) {
|
||||
// Invalidate and repaint
|
||||
cardImage = null;
|
||||
repaint();
|
||||
final int stamp = ++updateArtImageStamp;
|
||||
Util.threadPool.submit(() -> {
|
||||
try {
|
||||
final BufferedImage srcImage;
|
||||
srcImage = ImageCache.getCardImage(card, getCardWidth(), getCardHeight()).getImage();
|
||||
|
||||
// Register for download notification if image is missing
|
||||
if (srcImage == null) {
|
||||
registerDownloadListener(cardName, setCode, cardNumber);
|
||||
}
|
||||
|
||||
UI.invokeLater(() -> {
|
||||
if (stamp == updateArtImageStamp) {
|
||||
artImage = srcImage;
|
||||
cardRenderer.setArtImage(srcImage);
|
||||
if (srcImage != null) {
|
||||
// Invalidate and repaint
|
||||
cardImage = null;
|
||||
repaint();
|
||||
|
||||
// Unregister listener if we now have an image
|
||||
if (downloadListener != null) {
|
||||
OnDemandImageDownloader.getInstance().removeDownloadListener(downloadListener);
|
||||
downloadListener = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception | Error e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (Exception | Error e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener to refresh this card when its image is downloaded.
|
||||
*/
|
||||
private void registerDownloadListener(String cardName, String setCode, String cardNumber) {
|
||||
// Only register if auto-download is enabled
|
||||
if (!PreferencesDialog.isAutoDownloadEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloadListener != null) {
|
||||
return; // Already registered
|
||||
}
|
||||
|
||||
LOGGER.debug("MTGO: Registering download listener for: " + cardName + " [" + setCode + "/" + cardNumber + "]");
|
||||
|
||||
downloadListener = downloadedCard -> {
|
||||
// Check if this download matches our card
|
||||
boolean matches = downloadedCard.getName().equals(cardName)
|
||||
&& downloadedCard.getSet().equals(setCode)
|
||||
&& downloadedCard.getCollectorId().equals(cardNumber);
|
||||
|
||||
if (matches) {
|
||||
// Refresh this card's image on the EDT
|
||||
LOGGER.debug("MTGO: Triggering updateArtImage for: " + cardName);
|
||||
UI.invokeLater(this::updateArtImage);
|
||||
}
|
||||
};
|
||||
|
||||
OnDemandImageDownloader.getInstance().addDownloadListener(downloadListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanUp() {
|
||||
super.cleanUp();
|
||||
|
||||
// Unregister download listener
|
||||
if (downloadListener != null) {
|
||||
OnDemandImageDownloader.getInstance().removeDownloadListener(downloadListener);
|
||||
downloadListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,12 +88,19 @@ public final class ImageCache {
|
|||
|
||||
// try unknown token image
|
||||
if (tokenFile == null || !tokenFile.exists()) {
|
||||
// Queue token for on-demand download
|
||||
OnDemandImageDownloader.getInstance().queueDownload(info);
|
||||
// TODO: replace empty token by other default card, not cardback
|
||||
path = CardImageUtils.buildImagePathToDefault(DirectLinksForDownload.cardbackFilename);
|
||||
}
|
||||
} else {
|
||||
// CARD
|
||||
path = CardImageUtils.buildImagePathToCardOrToken(info);
|
||||
TFile cardFile = getTFile(path);
|
||||
if (cardFile == null || !cardFile.exists()) {
|
||||
// Queue card for on-demand download
|
||||
OnDemandImageDownloader.getInstance().queueDownload(info);
|
||||
}
|
||||
}
|
||||
|
||||
TFile file = getTFile(path);
|
||||
|
|
@ -371,4 +378,33 @@ public final class ImageCache {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all cache entries for a card so it will be reloaded from disk on next access.
|
||||
* Used after on-demand image download completes.
|
||||
*
|
||||
* @param name card name
|
||||
* @param setCode set code
|
||||
* @param collectorId collector number
|
||||
*/
|
||||
public static void invalidateCard(String name, String setCode, String collectorId) {
|
||||
// Key format: imageFileName#setCode#imageNumber#cardNumber#imageSize#usesVariousArt
|
||||
// We need to find and invalidate all keys matching this card
|
||||
String keyPattern = "#" + setCode + "#";
|
||||
String cardNumberPattern = "#" + collectorId + "#";
|
||||
|
||||
int cacheSize = SHARED_CARD_IMAGES_CACHE.asMap().size();
|
||||
LOGGER.debug("ImageCache invalidateCard: " + name + " [" + setCode + "/" + collectorId + "] cache size before: " + cacheSize);
|
||||
|
||||
// Iterate through cache and invalidate matching entries
|
||||
int removed = 0;
|
||||
for (String key : SHARED_CARD_IMAGES_CACHE.asMap().keySet()) {
|
||||
if (key.startsWith(name + "#") && key.contains(keyPattern) && key.contains(cardNumberPattern)) {
|
||||
LOGGER.debug("ImageCache removing key: " + key);
|
||||
SHARED_CARD_IMAGES_CACHE.invalidate(key);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
LOGGER.debug("ImageCache invalidateCard: removed " + removed + " entries");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,280 @@
|
|||
package org.mage.plugins.card.images;
|
||||
|
||||
import mage.client.dialog.PreferencesDialog;
|
||||
import mage.client.remote.XmageURLConnection;
|
||||
import mage.util.XmageThreadFactory;
|
||||
import net.java.truevfs.access.TFile;
|
||||
import net.java.truevfs.access.TFileOutputStream;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.mage.plugins.card.dl.sources.CardImageSource;
|
||||
import org.mage.plugins.card.dl.sources.CardImageUrls;
|
||||
import org.mage.plugins.card.dl.sources.ScryfallImageSourceNormal;
|
||||
import org.mage.plugins.card.utils.CardImageUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir;
|
||||
|
||||
/**
|
||||
* On-demand image downloader service.
|
||||
* Downloads missing card images automatically when they are first displayed.
|
||||
* Uses Scryfall as the image source with normal quality.
|
||||
*
|
||||
* @author Claude
|
||||
*/
|
||||
public class OnDemandImageDownloader {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(OnDemandImageDownloader.class);
|
||||
|
||||
private static final OnDemandImageDownloader instance = new OnDemandImageDownloader();
|
||||
|
||||
// Track cards that have been queued or downloaded to avoid duplicates
|
||||
private final Set<String> queuedOrDownloaded = ConcurrentHashMap.newKeySet();
|
||||
|
||||
// Download queue
|
||||
private final BlockingQueue<CardDownloadData> downloadQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
// Single-threaded executor for downloads (to respect rate limits)
|
||||
private final ExecutorService executor;
|
||||
|
||||
// Image source - use normal quality Scryfall
|
||||
private final CardImageSource imageSource = ScryfallImageSourceNormal.getInstance();
|
||||
|
||||
// Minimum file size to consider download successful
|
||||
private static final int MIN_FILE_SIZE_OF_GOOD_IMAGE = 1024 * 6;
|
||||
|
||||
// Listeners notified when an image is downloaded
|
||||
private final Set<Consumer<CardDownloadData>> downloadListeners = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private OnDemandImageDownloader() {
|
||||
executor = Executors.newSingleThreadExecutor(
|
||||
new XmageThreadFactory("OnDemandImageDownloader", true)
|
||||
);
|
||||
executor.submit(this::downloadWorker);
|
||||
}
|
||||
|
||||
public static OnDemandImageDownloader getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a card for download if not already queued/downloaded.
|
||||
* Only queues if auto-download is enabled in preferences.
|
||||
*
|
||||
* @param card The card to download
|
||||
*/
|
||||
public void queueDownload(CardDownloadData card) {
|
||||
// Check if feature is enabled
|
||||
if (!PreferencesDialog.isAutoDownloadEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (card == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create unique key for this card
|
||||
String key = createKey(card);
|
||||
|
||||
// Only queue if not already queued/downloaded
|
||||
if (queuedOrDownloaded.add(key)) {
|
||||
downloadQueue.offer(card);
|
||||
logger.debug("Queued for on-demand download: " + card.getName() + " (" + card.getSet() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private String createKey(CardDownloadData card) {
|
||||
return card.getName() + "#" + card.getSet() + "#" + card.getCollectorId() + "#" + card.isToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Background worker that processes the download queue
|
||||
*/
|
||||
private void downloadWorker() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
CardDownloadData card = downloadQueue.take();
|
||||
downloadCard(card);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error in download worker: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a single card image
|
||||
*
|
||||
* @param card The card to download
|
||||
* @return true if download was successful
|
||||
*/
|
||||
private boolean downloadCard(CardDownloadData card) {
|
||||
TFile fileTempImage = null;
|
||||
TFile destFile = null;
|
||||
|
||||
try {
|
||||
// Generate download URLs
|
||||
CardImageUrls urls;
|
||||
if (card.isToken()) {
|
||||
urls = imageSource.generateTokenUrl(card);
|
||||
} else {
|
||||
urls = imageSource.generateCardUrl(card);
|
||||
}
|
||||
|
||||
if (urls == null) {
|
||||
logger.debug("No download URL available for: " + card.getName() + " (" + card.getSet() + ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
String tempPath = getImagesDir() + File.separator + "downloading" + File.separator;
|
||||
fileTempImage = new TFile(tempPath + CardImageUtils.prepareCardNameForFile(card.getName()) + "-" + card.hashCode() + ".jpg");
|
||||
TFile parentFile = fileTempImage.getParentFile();
|
||||
if (parentFile != null && !parentFile.exists()) {
|
||||
parentFile.mkdirs();
|
||||
}
|
||||
|
||||
// Destination file
|
||||
destFile = new TFile(CardImageUtils.buildImagePathToCardOrToken(card));
|
||||
|
||||
// Skip if file already exists (might have been downloaded by another means)
|
||||
if (destFile.exists() && destFile.length() >= MIN_FILE_SIZE_OF_GOOD_IMAGE) {
|
||||
logger.debug("Image already exists: " + card.getName() + " (" + card.getSet() + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to download from available URLs
|
||||
List<String> downloadUrls = urls.getDownloadList();
|
||||
XmageURLConnection connection = null;
|
||||
boolean isDownloadOK = false;
|
||||
|
||||
for (String currentUrl : downloadUrls) {
|
||||
// Rate limiting
|
||||
imageSource.doPause(currentUrl);
|
||||
|
||||
connection = new XmageURLConnection(currentUrl);
|
||||
connection.startConnection();
|
||||
if (connection.isConnected()) {
|
||||
connection.setRequestHeaders(imageSource.getHttpRequestHeaders(currentUrl));
|
||||
|
||||
try {
|
||||
connection.connect();
|
||||
} catch (SocketException | UnknownHostException e) {
|
||||
logger.debug("Network error downloading " + card.getName() + ": " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode == 200) {
|
||||
isDownloadOK = true;
|
||||
break;
|
||||
} else {
|
||||
logger.debug("HTTP " + responseCode + " for " + card.getName() + " from " + currentUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the downloaded file
|
||||
if (isDownloadOK && connection != null && connection.isConnected()) {
|
||||
try (InputStream in = new BufferedInputStream(connection.getGoodResponseAsStream());
|
||||
OutputStream tempFileStream = new TFileOutputStream(fileTempImage);
|
||||
OutputStream out = new BufferedOutputStream(tempFileStream)) {
|
||||
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and move to final destination
|
||||
if (fileTempImage.exists() && fileTempImage.length() >= MIN_FILE_SIZE_OF_GOOD_IMAGE) {
|
||||
if (!destFile.getParentFile().exists()) {
|
||||
destFile.getParentFile().mkdirs();
|
||||
}
|
||||
fileTempImage.cp_rp(destFile);
|
||||
logger.debug("Downloaded: " + card.getName() + " (" + card.getSet() + ")");
|
||||
|
||||
// Invalidate cache so next access reloads from disk
|
||||
logger.debug("Invalidating cache for: " + card.getName() + " [" + card.getSet() + "/" + card.getCollectorId() + "]");
|
||||
ImageCache.invalidateCard(card.getName(), card.getSet(), card.getCollectorId());
|
||||
|
||||
// Notify listeners that image is available
|
||||
logger.debug("Notifying " + downloadListeners.size() + " listeners for: " + card.getName());
|
||||
notifyListeners(card);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
logger.debug("Downloaded file too small for: " + card.getName());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (AccessDeniedException e) {
|
||||
logger.error("Access denied downloading " + card.getName() + ": " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Error downloading " + card.getName() + ": " + e.getMessage(), e);
|
||||
} finally {
|
||||
// Cleanup temp file
|
||||
if (fileTempImage != null && fileTempImage.exists()) {
|
||||
try {
|
||||
TFile.rm(fileTempImage);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Could not delete temp file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the tracking set (useful for testing or reset)
|
||||
*/
|
||||
public void clearTracking() {
|
||||
queuedOrDownloaded.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener to be notified when an image is downloaded.
|
||||
* The listener receives the CardDownloadData of the downloaded card.
|
||||
*
|
||||
* @param listener the listener to add
|
||||
*/
|
||||
public void addDownloadListener(Consumer<CardDownloadData> listener) {
|
||||
downloadListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a download listener.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
public void removeDownloadListener(Consumer<CardDownloadData> listener) {
|
||||
downloadListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all listeners that an image was downloaded.
|
||||
*/
|
||||
private void notifyListeners(CardDownloadData card) {
|
||||
int count = 0;
|
||||
for (Consumer<CardDownloadData> listener : downloadListeners) {
|
||||
try {
|
||||
listener.accept(card);
|
||||
count++;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error notifying download listener: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
logger.debug("Notified " + count + " listeners for " + card.getName());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue