mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 04:38:00 +00:00
GuiDownload uses map of (local_path -> url) as core object
Download code uses NIO
This commit is contained in:
@@ -18,14 +18,14 @@
|
|||||||
package forge.gui.download;
|
package forge.gui.download;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.Map;
|
||||||
import java.util.HashSet;
|
import java.util.TreeMap;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import forge.ImageCache;
|
import forge.ImageCache;
|
||||||
import forge.card.CardRules;
|
import forge.card.CardRules;
|
||||||
|
import forge.card.CardSplitType;
|
||||||
import forge.item.CardDb;
|
import forge.item.CardDb;
|
||||||
import forge.item.CardPrinted;
|
import forge.item.CardPrinted;
|
||||||
import forge.properties.NewConstants;
|
import forge.properties.NewConstants;
|
||||||
@@ -37,18 +37,17 @@ public class GuiDownloadPicturesLQ extends GuiDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected final ArrayList<DownloadObject> getNeededImages() {
|
protected final Map<String, String> getNeededImages() {
|
||||||
ArrayList<DownloadObject> downloads = new ArrayList<DownloadObject>();
|
Map<String, String> downloads = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
|
||||||
Set<String> filenames = new HashSet<String>();
|
|
||||||
|
|
||||||
for (CardPrinted c : CardDb.instance().getAllCards()) {
|
for (CardPrinted c : CardDb.instance().getAllCards()) {
|
||||||
addDLObject(c, false, downloads, filenames);
|
addDLObject(c, downloads, false);
|
||||||
addDLObject(c, true, downloads, filenames);
|
if ( c.getRules().getSplitType() == CardSplitType.Transform)
|
||||||
|
addDLObject(c, downloads, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (CardPrinted c : CardDb.variants().getAllCards()) {
|
for (CardPrinted c : CardDb.variants().getAllCards()) {
|
||||||
addDLObject(c, false, downloads, filenames);
|
addDLObject(c, downloads, false);
|
||||||
addDLObject(c, true, downloads, filenames);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add missing tokens to the list of things to download.
|
// Add missing tokens to the list of things to download.
|
||||||
@@ -57,26 +56,37 @@ public class GuiDownloadPicturesLQ extends GuiDownloader {
|
|||||||
return downloads;
|
return downloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDLObject(CardPrinted c, boolean backFace, ArrayList<DownloadObject> downloads, Set<String> filenames) {
|
private void addDLObject(CardPrinted c, Map<String, String> downloads, boolean backFace) {
|
||||||
CardRules cardRules = c.getRules();
|
CardRules cardRules = c.getRules();
|
||||||
String urls = backFace ? cardRules.getPictureOtherSideUrl() : cardRules.getPictureUrl();
|
String urls = backFace ? cardRules.getPictureOtherSideUrl() : cardRules.getPictureUrl();
|
||||||
if (StringUtils.isEmpty(urls)) {
|
if (StringUtils.isEmpty(urls)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String filename = ImageCache.getImageKey(c, backFace, false);
|
||||||
for (String url : urls.split("\\\\")) {
|
File destFile = new File(NewConstants.CACHE_CARD_PICS_DIR, filename + ".jpg");
|
||||||
|
if (destFile.exists())
|
||||||
String filename = ImageCache.getImageKey(c, backFace, false);
|
return;
|
||||||
if (filenames.contains(filename)) {
|
|
||||||
continue;
|
filename = destFile.getAbsolutePath();
|
||||||
}
|
|
||||||
filenames.add(filename);
|
if (downloads.containsKey(filename)) {
|
||||||
|
return;
|
||||||
File destFile = new File(NewConstants.CACHE_CARD_PICS_DIR, filename + ".jpg");
|
|
||||||
if (!destFile.exists()) {
|
|
||||||
downloads.add(new DownloadObject(url, destFile));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final String urlToDownload;
|
||||||
|
int urlIndex = 0;
|
||||||
|
int allUrlsLen = 1;
|
||||||
|
if (urls.indexOf("\\\\") < 0)
|
||||||
|
urlToDownload = urls;
|
||||||
|
else {
|
||||||
|
String[] allUrls = urls.split("\\\\");
|
||||||
|
allUrlsLen = allUrls.length;
|
||||||
|
urlIndex = c.getArtIndex() % allUrlsLen;
|
||||||
|
urlToDownload = allUrls[urlIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
//System.out.println(c.getName() + "|" + c.getEdition() + " - " + c.getArtIndex() + " -> " + urlIndex + "/" + allUrlsLen + " === " + filename + " <<< " + urlToDownload);
|
||||||
|
downloads.put(destFile.getAbsolutePath(), urlToDownload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package forge.gui.download;
|
package forge.gui.download;
|
||||||
|
|
||||||
import java.io.File;
|
import java.util.HashMap;
|
||||||
import java.util.ArrayList;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
|
|
||||||
import forge.properties.NewConstants;
|
import forge.properties.NewConstants;
|
||||||
|
|
||||||
@@ -31,8 +29,9 @@ public class GuiDownloadPrices extends GuiDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ArrayList<DownloadObject> getNeededImages() {
|
protected Map<String, String> getNeededImages() {
|
||||||
final File f = new File(NewConstants.QUEST_CARD_PRICE_FILE);
|
Map<String, String> result = new HashMap<String, String>();
|
||||||
return Lists.newArrayList(new DownloadObject(NewConstants.URL_PRICE_DOWNLOAD, f));
|
result.put(NewConstants.QUEST_CARD_PRICE_FILE, NewConstants.URL_PRICE_DOWNLOAD);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
package forge.gui.download;
|
package forge.gui.download;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import forge.properties.NewConstants;
|
import forge.properties.NewConstants;
|
||||||
|
|
||||||
@@ -41,9 +42,9 @@ public class GuiDownloadQuestImages extends GuiDownloader {
|
|||||||
* @return an array of {@link forge.gui.download.GuiDownloadSetPicturesLQ} objects.
|
* @return an array of {@link forge.gui.download.GuiDownloadSetPicturesLQ} objects.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected final ArrayList<DownloadObject> getNeededImages() {
|
protected final Map<String, String> getNeededImages() {
|
||||||
// read all card names and urls
|
// read all card names and urls
|
||||||
final ArrayList<DownloadObject> urls = new ArrayList<DownloadObject>();
|
final Map<String, String> urls = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
|
||||||
|
|
||||||
addMissingItems(urls, NewConstants.IMAGE_LIST_QUEST_OPPONENT_ICONS_FILE, NewConstants.CACHE_ICON_PICS_DIR);
|
addMissingItems(urls, NewConstants.IMAGE_LIST_QUEST_OPPONENT_ICONS_FILE, NewConstants.CACHE_ICON_PICS_DIR);
|
||||||
addMissingItems(urls, NewConstants.IMAGE_LIST_QUEST_PET_SHOP_ICONS_FILE, NewConstants.CACHE_ICON_PICS_DIR);
|
addMissingItems(urls, NewConstants.IMAGE_LIST_QUEST_PET_SHOP_ICONS_FILE, NewConstants.CACHE_ICON_PICS_DIR);
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
package forge.gui.download;
|
package forge.gui.download;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
@@ -38,8 +39,8 @@ public class GuiDownloadSetPicturesLQ extends GuiDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected final ArrayList<DownloadObject> getNeededImages() {
|
protected final Map<String, String> getNeededImages() {
|
||||||
ArrayList<DownloadObject> downloads = new ArrayList<DownloadObject>();
|
Map<String, String> downloads = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
|
||||||
|
|
||||||
for (final CardPrinted c : Iterables.concat(CardDb.instance().getAllCards(), CardDb.variants().getAllCards())) {
|
for (final CardPrinted c : Iterables.concat(CardDb.instance().getAllCards(), CardDb.variants().getAllCards())) {
|
||||||
final String setCode3 = c.getEdition();
|
final String setCode3 = c.getEdition();
|
||||||
@@ -62,10 +63,11 @@ public class GuiDownloadSetPicturesLQ extends GuiDownloader {
|
|||||||
return downloads;
|
return downloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDLObject(String urlPath, String filename, ArrayList<DownloadObject> downloads) {
|
private void addDLObject(String urlPath, String filename, Map<String, String> downloads) {
|
||||||
File destFile = new File(NewConstants.CACHE_CARD_PICS_DIR, filename + ".jpg");
|
File destFile = new File(NewConstants.CACHE_CARD_PICS_DIR, filename + ".jpg");
|
||||||
|
// System.out.println(filename);
|
||||||
if (!destFile.exists()) {
|
if (!destFile.exists()) {
|
||||||
downloads.add(new DownloadObject(NewConstants.URL_PIC_DOWNLOAD + urlPath, destFile));
|
downloads.put(destFile.getAbsolutePath(), NewConstants.URL_PIC_DOWNLOAD + urlPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ package forge.gui.download;
|
|||||||
import java.awt.EventQueue;
|
import java.awt.EventQueue;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
@@ -31,7 +29,10 @@ import java.net.InetSocketAddress;
|
|||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.Proxy;
|
import java.net.Proxy;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import javax.swing.AbstractButton;
|
import javax.swing.AbstractButton;
|
||||||
@@ -107,8 +108,7 @@ public abstract class GuiDownloader extends DefaultBoundedRangeModel implements
|
|||||||
private int type;
|
private int type;
|
||||||
|
|
||||||
// Progress variables
|
// Progress variables
|
||||||
private ArrayList<DownloadObject> cards;
|
private Map<String, String> cards; // local path -> url
|
||||||
private int card;
|
|
||||||
private boolean cancel;
|
private boolean cancel;
|
||||||
private final long[] times = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
private final long[] times = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||||
private int tptr = 0;
|
private int tptr = 0;
|
||||||
@@ -226,7 +226,6 @@ public abstract class GuiDownloader extends DefaultBoundedRangeModel implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void update(final int card) {
|
private void update(final int card) {
|
||||||
this.card = card;
|
|
||||||
|
|
||||||
final class Worker implements Runnable {
|
final class Worker implements Runnable {
|
||||||
private final int card;
|
private final int card;
|
||||||
@@ -248,22 +247,19 @@ public abstract class GuiDownloader extends DefaultBoundedRangeModel implements
|
|||||||
|
|
||||||
long t2Go = (GuiDownloader.this.cards.size() - this.card) * a;
|
long t2Go = (GuiDownloader.this.cards.size() - this.card) * a;
|
||||||
|
|
||||||
boolean secOnly = true;
|
|
||||||
if (t2Go > 3600000) {
|
if (t2Go > 3600000) {
|
||||||
sb.append(String.format("%02d:", t2Go / 3600000));
|
sb.append(String.format("%02d:", t2Go / 3600000));
|
||||||
t2Go = t2Go % 3600000;
|
t2Go = t2Go % 3600000;
|
||||||
secOnly = false;
|
|
||||||
}
|
}
|
||||||
if (t2Go > 60000) {
|
if (t2Go > 60000) {
|
||||||
sb.append(String.format("%02d:", t2Go / 60000));
|
sb.append(String.format("%02d:", t2Go / 60000));
|
||||||
t2Go = t2Go % 60000;
|
t2Go = t2Go % 60000;
|
||||||
secOnly = false;
|
|
||||||
}
|
|
||||||
if (!secOnly) {
|
|
||||||
sb.append(String.format("%02d remaining.", t2Go / 1000));
|
|
||||||
} else {
|
} else {
|
||||||
sb.append(String.format("0:%02d remaining.", t2Go / 1000));
|
sb.append("00:");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sb.append(String.format("%02d remaining.", t2Go / 1000));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
sb.append(String.format("%d of %d items finished! Please close!",
|
sb.append(String.format("%d of %d items finished! Please close!",
|
||||||
this.card, GuiDownloader.this.cards.size()));
|
this.card, GuiDownloader.this.cards.size()));
|
||||||
@@ -281,11 +277,8 @@ public abstract class GuiDownloader extends DefaultBoundedRangeModel implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final void run() {
|
public final void run() {
|
||||||
BufferedInputStream in;
|
|
||||||
BufferedOutputStream out;
|
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
final Random r = MyRandom.getRandom();
|
||||||
|
|
||||||
Proxy p = null;
|
Proxy p = null;
|
||||||
if (this.type == 0) {
|
if (this.type == 0) {
|
||||||
p = Proxy.NO_PROXY;
|
p = Proxy.NO_PROXY;
|
||||||
@@ -301,81 +294,72 @@ public abstract class GuiDownloader extends DefaultBoundedRangeModel implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p != null) {
|
int iCard = 0;
|
||||||
final byte[] buf = new byte[1024];
|
for(Entry<String, String> kv : cards.entrySet()) {
|
||||||
int len;
|
if( cancel )
|
||||||
for (this.update(0); (this.card < this.cards.size()) && !this.cancel; this.update(this.card + 1)) {
|
break;
|
||||||
final String url = this.cards.get(this.card).getSource();
|
update(iCard++);
|
||||||
final File fileDest = this.cards.get(this.card).getDestination();
|
|
||||||
final File base = fileDest.getParentFile();
|
String url = kv.getValue();
|
||||||
|
final File fileDest = new File(kv.getKey());
|
||||||
|
final File base = fileDest.getParentFile();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// test for folder existence
|
// test for folder existence
|
||||||
if (!base.exists() && !base.mkdir()) { // create folder if not found
|
if (!base.exists() && !base.mkdir()) { // create folder if not found
|
||||||
System.out.println("Can't create folder" + base.getAbsolutePath());
|
System.out.println("Can't create folder" + base.getAbsolutePath());
|
||||||
}
|
|
||||||
|
|
||||||
URL imageUrl = new URL(url);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();
|
|
||||||
// don't allow redirections here -- they indicate 'file not found' on the server
|
|
||||||
conn.setInstanceFollowRedirects(false);
|
|
||||||
conn.connect();
|
|
||||||
|
|
||||||
if (conn.getResponseCode() != 200) {
|
|
||||||
conn.disconnect();
|
|
||||||
System.out.println("Skipped Download for: " + fileDest.getPath());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
in = new BufferedInputStream(conn.getInputStream());
|
|
||||||
out = new BufferedOutputStream(new FileOutputStream(fileDest));
|
|
||||||
|
|
||||||
while ((len = in.read(buf)) != -1) {
|
|
||||||
// user cancelled
|
|
||||||
if (this.cancel) {
|
|
||||||
in.close();
|
|
||||||
out.flush();
|
|
||||||
out.close();
|
|
||||||
|
|
||||||
// delete what was written so far
|
|
||||||
fileDest.delete();
|
|
||||||
this.close();
|
|
||||||
return;
|
|
||||||
} // if - cancel
|
|
||||||
|
|
||||||
out.write(buf, 0, len);
|
|
||||||
} // while - read and write file
|
|
||||||
in.close();
|
|
||||||
out.flush();
|
|
||||||
out.close();
|
|
||||||
} catch (final ConnectException ce) {
|
|
||||||
System.out.println("Connection refused for url: " + url);
|
|
||||||
} catch (final MalformedURLException mURLe) {
|
|
||||||
System.out.println("Error - possibly missing URL for: " + fileDest.getName());
|
|
||||||
} catch (final FileNotFoundException fnfe) {
|
|
||||||
String formatStr = "Error - the LQ picture %s could not be found on the server. [%s] - %s";
|
|
||||||
System.out.println(String.format(formatStr, fileDest.getName(), url, fnfe.getMessage()));
|
|
||||||
} catch (final Exception ex) {
|
|
||||||
Log.error("LQ Pictures", "Error downloading pictures", ex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// throttle to reduce load on the server
|
URL imageUrl = new URL(url);
|
||||||
try {
|
HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection(p);
|
||||||
Thread.sleep(r.nextInt(250) + 250);
|
// don't allow redirections here -- they indicate 'file not found' on the server
|
||||||
} catch (final InterruptedException e) {
|
conn.setInstanceFollowRedirects(false);
|
||||||
Log.error("GuiDownloader", "Sleep Error", e);
|
conn.connect();
|
||||||
|
|
||||||
|
if (conn.getResponseCode() != 200) {
|
||||||
|
conn.disconnect();
|
||||||
|
System.out.println("Skipped Download for: " + fileDest.getPath());
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} // for
|
|
||||||
}
|
ReadableByteChannel rbc = Channels.newChannel(conn.getInputStream());
|
||||||
|
FileOutputStream fos = new FileOutputStream(fileDest);
|
||||||
|
fos.getChannel().transferFrom(rbc, 0, 1 << 24);
|
||||||
|
|
||||||
|
fos.flush();
|
||||||
|
fos.close();
|
||||||
|
rbc.close();
|
||||||
|
} catch (final ConnectException ce) {
|
||||||
|
System.out.println("Connection refused for url: " + url);
|
||||||
|
} catch (final MalformedURLException mURLe) {
|
||||||
|
System.out.println("Error - possibly missing URL for: " + fileDest.getName());
|
||||||
|
} catch (final FileNotFoundException fnfe) {
|
||||||
|
String formatStr = "Error - the LQ picture %s could not be found on the server. [%s] - %s";
|
||||||
|
System.out.println(String.format(formatStr, fileDest.getName(), url, fnfe.getMessage()));
|
||||||
|
} catch (final Exception ex) {
|
||||||
|
Log.error("LQ Pictures", "Error downloading pictures", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// throttle to reduce load on the server
|
||||||
|
try {
|
||||||
|
Thread.sleep(r.nextInt(50) + 50);
|
||||||
|
} catch (final InterruptedException e) {
|
||||||
|
Log.error("GuiDownloader", "Sleep Error", e);
|
||||||
|
}
|
||||||
|
} // for
|
||||||
|
if ( !cancel )
|
||||||
|
update(cards.size());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract ArrayList<DownloadObject> getNeededImages();
|
protected abstract Map<String, String> getNeededImages();
|
||||||
|
|
||||||
protected static void addMissingItems(ArrayList<DownloadObject> list, String nameUrlFile, String dir) {
|
protected static void addMissingItems(Map<String, String> list, String nameUrlFile, String dir) {
|
||||||
for (Pair<String, String> nameUrlPair : FileUtil.readNameUrlFile(nameUrlFile)) {
|
for (Pair<String, String> nameUrlPair : FileUtil.readNameUrlFile(nameUrlFile)) {
|
||||||
File f = new File(dir, nameUrlPair.getLeft());
|
File f = new File(dir, nameUrlPair.getLeft());
|
||||||
|
//System.out.println(f.getAbsolutePath());
|
||||||
if (!f.exists()) {
|
if (!f.exists()) {
|
||||||
list.add(new DownloadObject(nameUrlPair.getRight(), f));
|
list.put(f.getAbsolutePath(), nameUrlPair.getRight());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,24 +381,4 @@ public abstract class GuiDownloader extends DefaultBoundedRangeModel implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static class DownloadObject {
|
|
||||||
|
|
||||||
private final String source;
|
|
||||||
private final File destination;
|
|
||||||
|
|
||||||
DownloadObject(final String srcUrl, final File destFile) {
|
|
||||||
source = srcUrl;
|
|
||||||
destination = destFile;
|
|
||||||
//System.out.println(String.format("downloading %s to %s", srcUrl, destFile));
|
|
||||||
System.out.println(String.format("Preparing to download %s", destFile));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSource() {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getDestination() {
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user