Scryfall small images download (#12282)

- Add new option for downloading small images from scryfall

- Refactor/simplify CardImageUrls

---------

Co-authored-by: xenohedron <xenohedron@users.noreply.github.com>
This commit is contained in:
tiera3 2024-06-01 15:09:02 +10:00 committed by GitHub
parent af59ff2c5c
commit f253777f7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 122 additions and 75 deletions

View file

@ -1,80 +1,45 @@
package org.mage.plugins.card.dl.sources;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* @author JayDi85
*/
public class CardImageUrls {
public String baseUrl;
public List<String> alternativeUrls;
public CardImageUrls() {
this.baseUrl = null;
this.alternativeUrls = new ArrayList<>();
}
private final List<String> urls = new ArrayList<>();
public CardImageUrls(String baseUrl) {
this(baseUrl, null);
addUrl(baseUrl);
}
public CardImageUrls(String baseUrl, String alternativeUrl) {
this();
this.baseUrl = baseUrl;
if (alternativeUrl != null
&& !alternativeUrl.isEmpty()
&& !Objects.equals(baseUrl, alternativeUrl)) {
this.alternativeUrls.add(alternativeUrl);
public CardImageUrls(String... urls) {
for (String url : urls) {
addUrl(url);
}
}
public CardImageUrls(String baseUrl, String alternativeUrl , String nextaltUrl) {
this();
this.baseUrl = baseUrl;
if (alternativeUrl != null
&& !alternativeUrl.isEmpty()
&& !Objects.equals(baseUrl, alternativeUrl)) {
this.alternativeUrls.add(alternativeUrl);
}
if (nextaltUrl != null
&& !nextaltUrl.isEmpty()
&& !Objects.equals(baseUrl, nextaltUrl)) {
this.alternativeUrls.add(nextaltUrl);
public CardImageUrls(Collection<String> urls) {
for (String url : urls) {
addUrl(url);
}
}
public List<String> getDownloadList() {
List<String> downloadUrls = new ArrayList<>();
if (this.baseUrl != null && !this.baseUrl.isEmpty()) {
downloadUrls.add(this.baseUrl);
}
// no needs in base url duplicate
if (this.alternativeUrls != null) {
for (String url : this.alternativeUrls) {
if (!url.equals(this.baseUrl)) {
downloadUrls.add(url);
}
}
}
return downloadUrls;
return urls;
}
public void addAlternativeUrl(String url) {
if (url != null && !url.isEmpty()) {
this.alternativeUrls.add(url);
} else {
throw new IllegalArgumentException();
// for tests
public String getBaseUrl() {
return urls.stream().findFirst().orElse(null);
}
public void addUrl(String url) {
// ignore nulls and duplicates
if (url != null && !url.isEmpty() && !urls.contains(url)) {
this.urls.add(url);
}
}
}

View file

@ -21,16 +21,20 @@ import java.util.*;
/**
* @author JayDi85
*/
public enum ScryfallImageSource implements CardImageSource {
public class ScryfallImageSource implements CardImageSource {
instance;
private static final ScryfallImageSource instance = new ScryfallImageSource();
private static final Logger logger = Logger.getLogger(ScryfallImageSource.class);
private final Map<CardLanguage, String> languageAliases;
private CardLanguage currentLanguage = CardLanguage.ENGLISH; // working language
private final Map<CardDownloadData, String> preparedUrls = new HashMap<>();
private final int DOWNLOAD_TIMEOUT_MS = 100;
private static final int DOWNLOAD_TIMEOUT_MS = 100;
public static ScryfallImageSource getInstance() {
return instance;
}
ScryfallImageSource() {
// LANGUAGES
@ -52,7 +56,7 @@ public enum ScryfallImageSource implements CardImageSource {
private CardImageUrls innerGenerateURL(CardDownloadData card, boolean isToken) {
String prepared = preparedUrls.getOrDefault(card, null);
if (prepared != null) {
return new CardImageUrls(prepared, null);
return new CardImageUrls(prepared);
}
String defaultCode = CardLanguage.ENGLISH.getCode();
@ -144,11 +148,11 @@ public enum ScryfallImageSource implements CardImageSource {
String apiUrl = ScryfallImageSupportCards.findDirectDownloadLink(card.getSet(), card.getName(), card.getCollectorId());
if (apiUrl != null) {
if (apiUrl.endsWith("*/")) {
apiUrl = apiUrl.substring(0 , apiUrl.length() -2) + "★/" ;
apiUrl = apiUrl.substring(0 , apiUrl.length() - 2) + "★/" ;
} else if (apiUrl.endsWith("+/")) {
apiUrl = apiUrl.substring(0 , apiUrl.length() -2) + "†/" ;
apiUrl = apiUrl.substring(0 , apiUrl.length() - 2) + "†/" ;
} else if (apiUrl.endsWith("Ph/")) {
apiUrl = apiUrl.substring(0 , apiUrl.length() -3) + "Φ/" ;
apiUrl = apiUrl.substring(0 , apiUrl.length() - 3) + "Φ/" ;
}
// BY DIRECT URL
// direct links via hardcoded API path. Used for cards with non-ASCII collector numbers

View file

@ -0,0 +1,45 @@
package org.mage.plugins.card.dl.sources;
import org.mage.plugins.card.images.CardDownloadData;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author tiera3
*/
public class ScryfallImageSourceSmall extends ScryfallImageSource {
private static final ScryfallImageSourceSmall instanceSmall = new ScryfallImageSourceSmall();
public static ScryfallImageSource getInstance() {
return instanceSmall;
}
private static String innerModifyUrlString(String oneUrl) {
return oneUrl.replaceFirst("/large/","/small/").replaceFirst("format=image","format=image&version=small");
}
private static CardImageUrls innerModifyUrl(CardImageUrls cardUrls) {
List<String> downloadUrls = cardUrls.getDownloadList().stream()
.map(ScryfallImageSourceSmall::innerModifyUrlString)
.collect(Collectors.toList());
return new CardImageUrls(downloadUrls);
}
@Override
public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception {
return innerModifyUrl(super.generateCardUrl(card));
}
@Override
public CardImageUrls generateTokenUrl(CardDownloadData card) throws Exception {
return innerModifyUrl(super.generateTokenUrl(card));
}
@Override
public float getAverageSize() {
return 13; // initial estimate - TODO calculate a more accurate number
}
}

View file

@ -76,7 +76,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
enum DownloadSources {
WIZARDS("1. wizards.com - low quality CARDS, multi-language, slow download", WizardCardsImageSource.instance),
TOKENS("2. tokens.mtg.onl - high quality TOKENS", TokensMtgImageSource.instance),
SCRYFALL("3. scryfall.com - high quality CARDS and TOKENS, multi-language", ScryfallImageSource.instance),
SCRYFALL("3. scryfall.com - high quality CARDS and TOKENS, multi-language", ScryfallImageSource.getInstance()),
SCRYFALL_SMALL("3a. scryfall.com small images - low quality CARDS and TOKENS, multi-language", ScryfallImageSourceSmall.getInstance()),
MAGIDEX("4. magidex.com - high quality CARDS", MagidexImageSource.instance),
GRAB_BAG("5. GrabBag - STAR WARS cards and tokens", GrabbagImageSource.instance),
MYTHICSPOILER("6. mythicspoiler.com", MythicspoilerComSource.instance),
@ -164,7 +165,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
// SOURCES - scryfall is default source
uiDialog.getSourcesCombo().setModel(new DefaultComboBoxModel(DownloadSources.values()));
uiDialog.getSourcesCombo().setSelectedItem(DownloadSources.SCRYFALL);
selectedSource = ScryfallImageSource.instance;
selectedSource = ScryfallImageSource.getInstance();
uiDialog.getSourcesCombo().addItemListener((ItemEvent event) -> {
if (event.getStateChange() == ItemEvent.SELECTED) {
comboboxSourceSelected(event);
@ -729,7 +730,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
DownloadTask(CardDownloadData card, String baseUrl, String actualFilename, int count) {
this.card = card;
this.urls = new CardImageUrls(baseUrl, null);
this.urls = new CardImageUrls(baseUrl);
this.count = count;
this.actualFilename = actualFilename;
this.useSpecifiedPaths = true;

View file

@ -6,6 +6,7 @@ import org.junit.Test;
import org.mage.plugins.card.dl.sources.CardImageSource;
import org.mage.plugins.card.dl.sources.CardImageUrls;
import org.mage.plugins.card.dl.sources.ScryfallImageSource;
import org.mage.plugins.card.dl.sources.ScryfallImageSourceSmall;
import org.mage.plugins.card.images.CardDownloadData;
/**
@ -15,25 +16,25 @@ public class ScryfallImagesDownloadTest {
@Test
public void test_Cards_DownloadLinks() throws Exception {
CardImageSource imageSource = ScryfallImageSource.instance;
CardImageSource imageSource = ScryfallImageSource.getInstance();
// normal card
CardImageUrls urls = imageSource.generateCardUrl(new CardDownloadData("Grizzly Bears", "10E", "268", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/10e/268/en?format=image", urls.baseUrl);
Assert.assertEquals("https://api.scryfall.com/cards/10e/268/en?format=image", urls.getBaseUrl());
// various card
urls = imageSource.generateCardUrl(new CardDownloadData("Grizzly Bears", "30A", "195", true, 1));
Assert.assertEquals("https://api.scryfall.com/cards/30a/195/en?format=image", urls.baseUrl);
Assert.assertEquals("https://api.scryfall.com/cards/30a/195/en?format=image", urls.getBaseUrl());
urls = imageSource.generateCardUrl(new CardDownloadData("Grizzly Bears", "30A", "492", true, 2));
Assert.assertEquals("https://api.scryfall.com/cards/30a/492/en?format=image", urls.baseUrl);
Assert.assertEquals("https://api.scryfall.com/cards/30a/492/en?format=image", urls.getBaseUrl());
// api link
urls = imageSource.generateCardUrl(new CardDownloadData("Ajani, the Greathearted", "WAR", "184*", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/war/184★/en?format=image", urls.baseUrl);
Assert.assertEquals("https://api.scryfall.com/cards/war/184★/en?format=image", urls.getBaseUrl());
// direct api link
urls = imageSource.generateCardUrl(new CardDownloadData("Command Tower", "REX", "26b", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/rex/26/en?format=image&face=back", urls.baseUrl);
Assert.assertEquals("https://api.scryfall.com/cards/rex/26/en?format=image&face=back", urls.getBaseUrl());
// the one ring
Assert.assertTrue("LTR must use The One Ring with 001 number, not 0", TheLordOfTheRingsTalesOfMiddleEarth.getInstance().getSetCardInfo()
@ -42,6 +43,37 @@ public class ScryfallImagesDownloadTest {
.anyMatch(c -> c.getCardNumber().equals("001"))
);
urls = imageSource.generateCardUrl(new CardDownloadData("The One Ring", "LTR", "001", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/ltr/0/en?format=image", urls.baseUrl);
Assert.assertEquals("https://api.scryfall.com/cards/ltr/0/en?format=image", urls.getBaseUrl());
// added same tests for small images
CardImageSource imageSourceSmall = ScryfallImageSourceSmall.getInstance();
// normal card
urls = imageSourceSmall.generateCardUrl(new CardDownloadData("Grizzly Bears", "10E", "268", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/10e/268/en?format=image&version=small", urls.getBaseUrl());
// various card
urls = imageSourceSmall.generateCardUrl(new CardDownloadData("Grizzly Bears", "30A", "195", true, 1));
Assert.assertEquals("https://api.scryfall.com/cards/30a/195/en?format=image&version=small", urls.getBaseUrl());
urls = imageSourceSmall.generateCardUrl(new CardDownloadData("Grizzly Bears", "30A", "492", true, 2));
Assert.assertEquals("https://api.scryfall.com/cards/30a/492/en?format=image&version=small", urls.getBaseUrl());
// api link
urls = imageSourceSmall.generateCardUrl(new CardDownloadData("Ajani, the Greathearted", "WAR", "184*", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/war/184★/en?format=image&version=small", urls.getBaseUrl());
// direct api link
urls = imageSourceSmall.generateCardUrl(new CardDownloadData("Command Tower", "REX", "26b", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/rex/26/en?format=image&version=small&face=back", urls.getBaseUrl());
// the one ring
Assert.assertTrue("LTR must use The One Ring with 001 number, not 0", TheLordOfTheRingsTalesOfMiddleEarth.getInstance().getSetCardInfo()
.stream()
.filter(c -> c.getName().equals("The One Ring"))
.anyMatch(c -> c.getCardNumber().equals("001"))
);
urls = imageSourceSmall.generateCardUrl(new CardDownloadData("The One Ring", "LTR", "001", false, 0));
Assert.assertEquals("https://api.scryfall.com/cards/ltr/0/en?format=image&version=small", urls.getBaseUrl());
}
}

View file

@ -19,15 +19,15 @@ public class TokensMtgImageSourceTest {
CardImageSource imageSource = TokensMtgImageSource.instance;
CardImageUrls url = imageSource.generateTokenUrl(new CardDownloadData("Thopter", "ORI", "0", false, 1));
Assert.assertEquals("https://tokens.mtg.onl/tokens/ORI_010-Thopter.jpg", url.baseUrl);
Assert.assertEquals("https://tokens.mtg.onl/tokens/ORI_010-Thopter.jpg", url.getBaseUrl());
url = imageSource.generateTokenUrl(new CardDownloadData("Thopter", "ORI", "0", false, 2));
Assert.assertEquals("https://tokens.mtg.onl/tokens/ORI_011-Thopter.jpg", url.baseUrl);
Assert.assertEquals("https://tokens.mtg.onl/tokens/ORI_011-Thopter.jpg", url.getBaseUrl());
url = imageSource.generateTokenUrl(new CardDownloadData("Ashaya, the Awoken World", "ORI", "0", false, 0));
Assert.assertEquals("https://tokens.mtg.onl/tokens/ORI_007-Ashaya,-the-Awoken-World.jpg", url.baseUrl);
Assert.assertEquals("https://tokens.mtg.onl/tokens/ORI_007-Ashaya,-the-Awoken-World.jpg", url.getBaseUrl());
url = imageSource.generateTokenUrl(new CardDownloadData("Emblem Gideon, Ally of Zendikar", "BFZ", "0", false, 0));
Assert.assertEquals("https://tokens.mtg.onl/tokens/BFZ_012-Gideon-Emblem.jpg", url.baseUrl);
Assert.assertEquals("https://tokens.mtg.onl/tokens/BFZ_012-Gideon-Emblem.jpg", url.getBaseUrl());
}
}