/*
* Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*/
package org.mage.plugins.card.dl.sources;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.prefs.Preferences;
import mage.client.MageFrame;
import mage.client.dialog.PreferencesDialog;
import mage.remote.Connection;
import mage.remote.Connection.ProxyType;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.mage.plugins.card.images.CardDownloadData;
/**
* @author North
*/
public enum WizardCardsImageSource implements CardImageSource {
instance;
private Map<String, String> setsAliases;
private Map<String, String> languageAliases;
private final Map<String, Map<String, String>> sets;
@Override
public String getSourceName() {
return "WOTC Gatherer";
}
WizardCardsImageSource() {
sets = new HashMap<>();
setsAliases = new HashMap<>();
setsAliases.put("2ED", "Unlimited Edition");
setsAliases.put("10E", "Tenth Edition");
setsAliases.put("3ED", "Revised Edition");
setsAliases.put("4ED", "Fourth Edition");
setsAliases.put("5DN", "Fifth Dawn");
setsAliases.put("5ED", "Fifth Edition");
setsAliases.put("6ED", "Classic Sixth Edition");
setsAliases.put("7ED", "Seventh Edition");
setsAliases.put("8ED", "Eighth Edition");
setsAliases.put("9ED", "Ninth Edition");
setsAliases.put("AER", "Aether Revolt");
setsAliases.put("AKH", "Amonkhet");
setsAliases.put("ALA", "Shards of Alara");
setsAliases.put("ALL", "Alliances");
setsAliases.put("ANB", "Archenemy: Nicol Bolas");
setsAliases.put("APC", "Apocalypse");
setsAliases.put("ARB", "Alara Reborn");
setsAliases.put("ARC", "Archenemy");
setsAliases.put("ARN", "Arabian Nights");
setsAliases.put("ATH", "Anthologies");
setsAliases.put("ATQ", "Antiquities");
setsAliases.put("AVR", "Avacyn Restored");
setsAliases.put("BFZ", "Battle for Zendikar");
setsAliases.put("BNG", "Born of the Gods");
setsAliases.put("BOK", "Betrayers of Kamigawa");
setsAliases.put("BRB", "Battle Royale Box Set");
setsAliases.put("BTD", "Beatdown Box Set");
setsAliases.put("C13", "Commander 2013 Edition");
setsAliases.put("C14", "Commander 2014");
setsAliases.put("C15", "Commander 2015");
setsAliases.put("C16", "Commander 2016");
setsAliases.put("CMA", "Commander Anthology");
setsAliases.put("CHK", "Champions of Kamigawa");
setsAliases.put("CHR", "Chronicles");
setsAliases.put("CMD", "Magic: The Gathering-Commander");
setsAliases.put("CNS", "Magic: The Gathering—Conspiracy");
setsAliases.put("CN2", "Conspiracy: Take the Crown");
setsAliases.put("CON", "Conflux");
setsAliases.put("CSP", "Coldsnap");
setsAliases.put("DD2", "Duel Decks: Jace vs. Chandra");
setsAliases.put("DD3DVD", "Duel Decks Anthology, Divine vs. Demonic");
setsAliases.put("DD3EVG", "Duel Decks Anthology, Elves vs. Goblins");
setsAliases.put("DD3GVL", "Duel Decks Anthology, Garruk vs. Liliana");
setsAliases.put("DD3JVC", "Duel Decks Anthology, Jace vs. Chandra");
setsAliases.put("DDC", "Duel Decks: Divine vs. Demonic");
setsAliases.put("DDD", "Duel Decks: Garruk vs. Liliana");
setsAliases.put("DDE", "Duel Decks: Phyrexia vs. the Coalition");
setsAliases.put("DDF", "Duel Decks: Elspeth vs. Tezzeret");
setsAliases.put("DDG", "Duel Decks: Knights vs. Dragons");
setsAliases.put("DDH", "Duel Decks: Ajani vs. Nicol Bolas");
setsAliases.put("DDI", "Duel Decks: Venser vs. Koth");
setsAliases.put("DDJ", "Duel Decks: Izzet vs. Golgari");
setsAliases.put("DDK", "Duel Decks: Sorin vs. Tibalt");
setsAliases.put("DDL", "Duel Decks: Heroes vs. Monsters");
setsAliases.put("DDM", "Duel Decks: Jace vs. Vraska");
setsAliases.put("DDN", "Duel Decks: Speed vs. Cunning");
setsAliases.put("DDO", "Duel Decks: Elspeth vs. Kiora");
setsAliases.put("DDP", "Duel Decks: Zendikar vs. Eldrazi");
setsAliases.put("DDQ", "Duel Decks: Blessed vs. Cursed");
setsAliases.put("DDR", "Duel Decks: Nissa vs. Ob Nixilis");
setsAliases.put("DDS", "Duel Decks: Mind vs. Might");
setsAliases.put("DGM", "Dragon's Maze");
setsAliases.put("DIS", "Dissension");
setsAliases.put("DKA", "Dark Ascension");
setsAliases.put("DKM", "Deckmasters");
setsAliases.put("DRB", "From the Vault: Dragons");
setsAliases.put("DRK", "The Dark");
setsAliases.put("DST", "Darksteel");
setsAliases.put("DTK", "Dragons of Tarkir");
setsAliases.put("EMN", "Eldritch Moon");
setsAliases.put("EMA", "Eternal Masters");
setsAliases.put("EVE", "Eventide");
setsAliases.put("EVG", "Duel Decks: Elves vs. Goblins");
setsAliases.put("EXO", "Exodus");
setsAliases.put("FEM", "Fallen Empires");
setsAliases.put("FNMP", "Friday Night Magic");
setsAliases.put("FRF", "Fate Reforged");
setsAliases.put("FUT", "Future Sight");
setsAliases.put("GPT", "Guildpact");
setsAliases.put("GPX", "Grand Prix");
setsAliases.put("GRC", "WPN Gateway");
setsAliases.put("GTC", "Gatecrash");
setsAliases.put("H09", "Premium Deck Series: Slivers");
setsAliases.put("HML", "Homelands");
setsAliases.put("HOP", "Planechase");
setsAliases.put("HOU", "Hour of Devastation");
setsAliases.put("ICE", "Ice Age");
setsAliases.put("INV", "Invasion");
setsAliases.put("ISD", "Innistrad");
setsAliases.put("JOU", "Journey into Nyx");
setsAliases.put("JR", "Judge Promo");
setsAliases.put("JUD", "Judgment");
setsAliases.put("KLD", "Kaladesh");
setsAliases.put("KTK", "Khans of Tarkir");
setsAliases.put("LEA", "Limited Edition Alpha");
setsAliases.put("LEB", "Limited Edition Beta");
setsAliases.put("LEG", "Legends");
setsAliases.put("LGN", "Legions");
setsAliases.put("LRW", "Lorwyn");
setsAliases.put("M10", "Magic 2010");
setsAliases.put("M11", "Magic 2011");
setsAliases.put("M12", "Magic 2012");
setsAliases.put("M13", "Magic 2013");
setsAliases.put("M14", "Magic 2014");
setsAliases.put("M15", "Magic 2015");
setsAliases.put("MBP", "Media Inserts");
setsAliases.put("MBS", "Mirrodin Besieged");
setsAliases.put("ME2", "Masters Edition II");
setsAliases.put("ME3", "Masters Edition III");
setsAliases.put("ME4", "Masters Edition IV");
setsAliases.put("MED", "Masters Edition");
// setsAliases.put("MGDC", "Game Day");
setsAliases.put("MIR", "Mirage");
setsAliases.put("MLP", "Launch Party");
setsAliases.put("MMA", "Modern Masters");
setsAliases.put("MM2", "Modern Masters 2015");
setsAliases.put("MM3", "Modern Masters 2017");
setsAliases.put("MMQ", "Mercadian Masques");
setsAliases.put("MOR", "Morningtide");
setsAliases.put("MPRP", "Magic Player Rewards");
setsAliases.put("MPS", "Masterpiece Series");
setsAliases.put("MRD", "Mirrodin");
setsAliases.put("NEM", "Nemesis");
setsAliases.put("NPH", "New Phyrexia");
setsAliases.put("OGW", "Oath of the Gatewatch");
setsAliases.put("ODY", "Odyssey");
setsAliases.put("ONS", "Onslaught");
setsAliases.put("ORI", "Magic Origins");
setsAliases.put("PC2", "Planechase 2012 Edition");
setsAliases.put("PCY", "Prophecy");
setsAliases.put("PD2", "Premium Deck Series: Fire and Lightning");
setsAliases.put("PLC", "Planar Chaos");
setsAliases.put("PLS", "Planeshift");
setsAliases.put("PO2", "Portal Second Age");
setsAliases.put("POR", "Portal");
setsAliases.put("PTC", "Prerelease Events");
setsAliases.put("PTK", "Portal Three Kingdoms");
setsAliases.put("RAV", "Ravnica: City of Guilds");
setsAliases.put("ROE", "Rise of the Eldrazi");
setsAliases.put("RTR", "Return to Ravnica");
setsAliases.put("S00", "Starter 2000");
setsAliases.put("S99", "Starter 1999");
setsAliases.put("SCG", "Scourge");
setsAliases.put("SHM", "Shadowmoor");
setsAliases.put("SOI", "Shadows over Innistrad");
setsAliases.put("SOK", "Saviors of Kamigawa");
setsAliases.put("SOM", "Scars of Mirrodin");
setsAliases.put("STH", "Stronghold");
setsAliases.put("THS", "Theros");
setsAliases.put("TMP", "Tempest");
setsAliases.put("TOR", "Torment");
setsAliases.put("TPR", "Tempest Remastered");
setsAliases.put("TSB", "Time Spiral \"Timeshifted\"");
setsAliases.put("TSP", "Time Spiral");
setsAliases.put("UDS", "Urza's Destiny");
setsAliases.put("UGL", "Unglued");
setsAliases.put("ULG", "Urza's Legacy");
setsAliases.put("UNH", "Unhinged");
setsAliases.put("USG", "Urza's Saga");
setsAliases.put("V09", "From the Vault: Exiled");
setsAliases.put("V10", "From the Vault: Relics");
setsAliases.put("V11", "From the Vault: Legends");
setsAliases.put("V12", "From the Vault: Realms");
setsAliases.put("V13", "From the Vault: Twenty");
setsAliases.put("V14", "From the Vault: Annihilation (2014)");
setsAliases.put("V15", "From the Vault: Angels (2015)");
setsAliases.put("V16", "From the Vault: Lore (2016)");
setsAliases.put("VG1", "Vanguard Set 1");
setsAliases.put("VG2", "Vanguard Set 2");
setsAliases.put("VG3", "Vanguard Set 3");
setsAliases.put("VG4", "Vanguard Set 4");
setsAliases.put("VGO", "MTGO Vanguard");
setsAliases.put("VIS", "Visions");
setsAliases.put("VMA", "Vintage Masters");
setsAliases.put("W16", "Welcome Deck 2016");
setsAliases.put("WMCQ", "World Magic Cup Qualifier");
setsAliases.put("WTH", "Weatherlight");
setsAliases.put("WWK", "Worldwake");
setsAliases.put("ZEN", "Zendikar");
languageAliases = new HashMap<>();
languageAliases.put("es", "Spanish");
languageAliases.put("jp", "Japanese");
languageAliases.put("it", "Italian");
languageAliases.put("fr", "French");
languageAliases.put("cn", "Chinese Simplified");
languageAliases.put("de", "German");
}
@Override
public String getNextHttpImageUrl() {
return null;
}
@Override
public String getFileForHttpImage(String httpImageUrl) {
return null;
}
private Map<String, String> getSetLinks(String cardSet) {
ConcurrentHashMap<String, String> setLinks = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10);
try {
String setNames = setsAliases.get(cardSet);
String preferedLanguage = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_PREF_LANGUAGE, "en");
for (String setName : setNames.split("\\^")) {
String URLSetName = URLEncoder.encode(setName, "UTF-8");
int page = 0;
int firstMultiverseIdLastPage = 0;
Pages:
while (page < 999) {
String searchUrl = "http://gatherer.wizards.com/Pages/Search/Default.aspx?page=" + page + "&output=spoiler&method=visual&action=advanced&set=+[%22" + URLSetName + "%22]";
Document doc = getDocument(searchUrl);
Elements cardsImages = doc.select("img[src^=../../Handlers/]");
if (cardsImages.isEmpty()) {
break;
}
for (int i = 0; i < cardsImages.size(); i++) {
Integer multiverseId = Integer.parseInt(cardsImages.get(i).attr("src").replaceAll("[^\\d]", ""));
if (i == 0) {
if (multiverseId == firstMultiverseIdLastPage) {
break Pages;
}
firstMultiverseIdLastPage = multiverseId;
}
String cardName = normalizeName(cardsImages.get(i).attr("alt"));
if (cardName != null && !cardName.isEmpty()) {
Runnable task = new GetImageLinkTask(multiverseId, cardName, preferedLanguage, setLinks);
executor.execute(task);
}
}
page++;
}
}
} catch (IOException ex) {
System.out.println("Exception when parsing the wizards page: " + ex.getMessage());
}
executor.shutdown();
while (!executor.isTerminated()) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ie) {
}
}
return setLinks;
}
private Document getDocument(String urlString) throws NumberFormatException, IOException {
Preferences prefs = MageFrame.getPreferences();
Connection.ProxyType proxyType = Connection.ProxyType.valueByText(prefs.get("proxyType", "None"));
Document doc;
if (proxyType == ProxyType.NONE) {
doc = Jsoup.connect(urlString).get();
} else {
String proxyServer = prefs.get("proxyAddress", "");
int proxyPort = Integer.parseInt(prefs.get("proxyPort", "0"));
URL url = new URL(urlString);
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyServer, proxyPort));
HttpURLConnection uc = (HttpURLConnection) url.openConnection(proxy);
uc.connect();
String line;
StringBuffer tmp = new StringBuffer();
BufferedReader in = new BufferedReader(new InputStreamReader(uc.getInputStream()));
while ((line = in.readLine()) != null) {
tmp.append(line);
}
doc = Jsoup.parse(String.valueOf(tmp));
}
return doc;
}
private Map<String, String> getLandVariations(int multiverseId, String cardName) throws IOException, NumberFormatException {
String urlLandDocument = "http://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=" + multiverseId;
Document landDoc = getDocument(urlLandDocument);
Elements variations = landDoc.select("a.variationlink");
Map<String, String> links = new HashMap<>();
if (!variations.isEmpty()) {
int landNumber = 1;
for (Element variation : variations) {
Integer landMultiverseId = Integer.parseInt(variation.attr("onclick").replaceAll("[^\\d]", ""));
links.put((cardName + landNumber).toLowerCase(), generateLink(landMultiverseId));
landNumber++;
}
} else {
links.put(cardName.toLowerCase(), generateLink(multiverseId));
}
return links;
}
private static String generateLink(int landMultiverseId) {
return "/Handlers/Image.ashx?multiverseid=" + landMultiverseId + "&type=card";
}
private int getLocalizedMultiverseId(String preferedLanguage, Integer multiverseId) throws IOException {
if (preferedLanguage.equals("en")) {
return multiverseId;
}
String languageName = languageAliases.get(preferedLanguage);
HashMap<String, Integer> localizedLanguageIds = getlocalizedMultiverseIds(multiverseId);
if (localizedLanguageIds.containsKey(languageName)) {
return localizedLanguageIds.get(languageName);
} else {
return multiverseId;
}
}
private HashMap<String, Integer> getlocalizedMultiverseIds(Integer englishMultiverseId) throws IOException {
String cardLanguagesUrl = "http://gatherer.wizards.com/Pages/Card/Languages.aspx?multiverseid=" + englishMultiverseId;
Document cardLanguagesDoc = getDocument(cardLanguagesUrl);
Elements languageTableRows = cardLanguagesDoc.select("tr.cardItem");
HashMap<String, Integer> localizedIds = new HashMap<>();
if (!languageTableRows.isEmpty()) {
for (Element languageTableRow : languageTableRows) {
Elements languageTableColumns = languageTableRow.select("td");
Integer localizedId = Integer.parseInt(languageTableColumns.get(0).select("a").first().attr("href").replaceAll("[^\\d]", ""));
String languageName = languageTableColumns.get(1).text().trim();
localizedIds.put(languageName, localizedId);
}
}
return localizedIds;
}
private String normalizeName(String name) {
//Split card
if (name.contains("//")) {
name = name.substring(0, name.indexOf('(') - 1);
}
//Special timeshifted name
if (name.startsWith("XX")) {
name = name.substring(name.indexOf('(') + 1, name.length() - 1);
}
return name.replace("\u2014", "-").replace("\u2019", "'")
.replace("\u00C6", "AE").replace("\u00E6", "ae")
.replace("\u00C3\u2020", "AE")
.replace("\u00C1", "A").replace("\u00E1", "a")
.replace("\u00C2", "A").replace("\u00E2", "a")
.replace("\u00D6", "O").replace("\u00F6", "o")
.replace("\u00DB", "U").replace("\u00FB", "u")
.replace("\u00DC", "U").replace("\u00FC", "u")
.replace("\u00E9", "e").replace("&", "//")
.replace("Hintreland Scourge", "Hinterland Scourge");
}
@Override
public String generateURL(CardDownloadData card) throws Exception {
String collectorId = card.getCollectorId();
String cardSet = card.getSet();
if (collectorId == null || cardSet == null) {
throw new Exception("Wrong parameters for image: collector id: " + collectorId + ",card set: " + cardSet);
}
if (card.isFlippedSide()) { //doesn't support rotated images
return null;
}
String setNames = setsAliases.get(cardSet);
if (setNames != null) {
Map<String, String> setLinks = sets.computeIfAbsent(cardSet, k -> getSetLinks(cardSet));
String link = setLinks.get(card.getDownloadName().toLowerCase());
if (link == null) {
int length = collectorId.length();
if (Character.isLetter(collectorId.charAt(length - 1))) {
length -= 1;
}
int number = Integer.parseInt(collectorId.substring(0, length));
if (setLinks.size() >= number) {
link = setLinks.get(Integer.toString(number - 1));
} else {
link = setLinks.get(Integer.toString(number - 21));
if (link != null) {
link = link.replace(Integer.toString(number - 20), (Integer.toString(number - 20) + 'a'));
}
}
}
if (link != null && !link.startsWith("http://")) {
link = "http://gatherer.wizards.com" + link;
}
return link;
}
return null;
}
@Override
public String generateTokenUrl(CardDownloadData card) {
return null;
}
@Override
public float getAverageSize() {
return 60.0f;
}
private final class GetImageLinkTask implements Runnable {
private final int multiverseId;
private final String cardName;
private final String preferedLanguage;
private final ConcurrentHashMap setLinks;
public GetImageLinkTask(int multiverseId, String cardName, String preferedLanguage, ConcurrentHashMap setLinks) {
this.multiverseId = multiverseId;
this.cardName = cardName;
this.preferedLanguage = preferedLanguage;
this.setLinks = setLinks;
}
@Override
public void run() {
try {
if (cardName.equals("Forest") || cardName.equals("Swamp") || cardName.equals("Mountain") || cardName.equals("Island") || cardName.equals("Plains")) {
setLinks.putAll(getLandVariations(multiverseId, cardName));
} else {
Integer preferedMultiverseId = getLocalizedMultiverseId(preferedLanguage, multiverseId);
setLinks.put(cardName.toLowerCase(), generateLink(preferedMultiverseId));
}
} catch (IOException | NumberFormatException ex) {
System.out.println("Exception when parsing the wizards page: " + ex.getMessage());
}
}
}
@Override
public int getTotalImages() {
return -1;
}
@Override
public boolean isTokenSource() {
return false;
}
@Override
public void doPause(String httpImageUrl) {
}
}