c278ea54b9f3c84ab38a430e0092d7402f80b776
[xudocci.git] / src / main / java / net / pterodactylus / xdcc / ui / stdin / CommandReader.java
1 /*
2  * XdccDownloader - CommandReader.java - Copyright © 2013 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.xdcc.ui.stdin;
19
20 import static net.pterodactylus.xdcc.data.Download.BY_NAME;
21 import static net.pterodactylus.xdcc.data.Download.BY_RUNNING;
22
23 import java.io.BufferedReader;
24 import java.io.IOException;
25 import java.io.Reader;
26 import java.io.Writer;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.Comparator;
31 import java.util.List;
32 import java.util.Set;
33 import java.util.regex.Pattern;
34
35 import net.pterodactylus.irc.Connection;
36 import net.pterodactylus.irc.DccReceiver;
37 import net.pterodactylus.irc.util.MessageCleaner;
38 import net.pterodactylus.xdcc.core.Core;
39 import net.pterodactylus.xdcc.core.event.DownloadFailed;
40 import net.pterodactylus.xdcc.core.event.DownloadFinished;
41 import net.pterodactylus.xdcc.core.event.DownloadStarted;
42 import net.pterodactylus.xdcc.core.event.GenericMessage;
43 import net.pterodactylus.xdcc.core.event.MessageReceived;
44 import net.pterodactylus.xdcc.data.Bot;
45 import net.pterodactylus.xdcc.data.Download;
46 import net.pterodactylus.xdcc.data.Pack;
47
48 import com.google.common.base.Predicate;
49 import com.google.common.collect.ComparisonChain;
50 import com.google.common.collect.FluentIterable;
51 import com.google.common.collect.Lists;
52 import com.google.common.collect.Ordering;
53 import com.google.common.collect.Sets;
54 import com.google.common.eventbus.Subscribe;
55 import com.google.common.primitives.Ints;
56 import com.google.common.util.concurrent.AbstractExecutionThreadService;
57
58 /**
59  * Command interface for arbitrary {@link Reader}s.
60  *
61  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
62  */
63 public class CommandReader extends AbstractExecutionThreadService {
64
65         /** The core being controlled. */
66         private final Core core;
67
68         /** The reader to read commands from. */
69         private final BufferedReader reader;
70
71         /** The writer to write the results to. */
72         private final Writer writer;
73
74         /**
75          * Creates a new command reader.
76          *
77          * @param core
78          *              The core being controlled
79          * @param reader
80          *              The reader to read commands from
81          * @param writer
82          *              The write to write results to
83          */
84         public CommandReader(Core core, Reader reader, Writer writer) {
85                 this.core = core;
86                 this.reader = new BufferedReader(reader);
87                 this.writer = writer;
88         }
89
90         //
91         // ABSTRACTEXECUTIONTHREADSERVICE METHODS
92         //
93
94         @Override
95         protected void run() throws Exception {
96                 String lastLine = "";
97                 String line;
98                 final List<Result> lastResult = Lists.newArrayList();
99                 final List<Download> downloads = Lists.newArrayList();
100                 final List<Connection> lastConnections = Lists.newArrayList();
101                 while ((line = reader.readLine()) != null) {
102                         if (line.equals("")) {
103                                 line = lastLine;
104                         }
105                         String[] words = line.split(" +");
106                         if (words[0].equalsIgnoreCase("search")) {
107                                 lastResult.clear();
108                                 for (Bot bot : Lists.newArrayList(core.bots())) {
109                                         for (Pack pack : Lists.newArrayList(bot)) {
110                                                 boolean found = true;
111                                                 for (int wordIndex = 1; wordIndex < words.length; ++wordIndex) {
112                                                         if (words[wordIndex].startsWith("-") && pack.name().toLowerCase().contains(words[wordIndex].toLowerCase().substring(1))) {
113                                                                 found = false;
114                                                                 break;
115                                                         }
116                                                         if (!words[wordIndex].startsWith("-") && !pack.name().toLowerCase().contains(words[wordIndex].toLowerCase())) {
117                                                                 found = false;
118                                                                 break;
119                                                         }
120                                                 }
121                                                 if (found) {
122                                                         lastResult.add(new Result(bot, pack));
123                                                 }
124                                         }
125                                 }
126                                 Collections.sort(lastResult);
127                                 int counter = 0;
128                                 for (Result result : lastResult) {
129                                         writeLine(String.format("[%d] %s (%s) from %s (#%s) on %s", counter++, result.pack().name(), result.pack().size(), result.bot().name(), result.pack().id(), result.bot().network().name()));
130                                 }
131                                 writeLine("End of Search.");
132                         } else if (words[0].equalsIgnoreCase("dcc")) {
133                                 int counter = 0;
134                                 downloads.clear();
135                                 downloads.addAll(FluentIterable.from(core.downloads()).toSortedList(Ordering.from(BY_NAME).compound(BY_RUNNING)));
136                                 for (Download download : downloads) {
137                                         DccReceiver dccReceiver = download.dccReceiver();
138                                         if (dccReceiver == null) {
139                                                 /* download has not even started. */
140                                                 writer.write(String.format("[%d] %s requested from %s (not started yet)\n", counter++, download.pack().name(), download.bot().name()));
141                                                 continue;
142                                         }
143                                         writer.write(String.format("[%d] %s from %s (%s, ", counter++, dccReceiver.filename(), download.bot().name(), f(dccReceiver.size())));
144                                         if (dccReceiver.isRunning()) {
145                                                 writer.write(String.format("%.1f%%, %s/s, %s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate()), getTimeLeft(dccReceiver)));
146                                         } else {
147                                                 if (dccReceiver.progress() >= dccReceiver.size()) {
148                                                         writer.write(String.format("complete, %s/s", f(dccReceiver.overallRate())));
149                                                 } else {
150                                                         writer.write(String.format("aborted at %.1f%%, %s/s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate())));
151                                                 }
152                                         }
153                                         writer.write(")\n");
154                                 }
155                                 writeLine("End of DCCs.");
156                         } else if (words[0].equalsIgnoreCase("get")) {
157                                 Integer index = Ints.tryParse(words[1]);
158                                 if ((index != null) && (index < lastResult.size())) {
159                                         core.fetch(lastResult.get(index).bot(), lastResult.get(index).pack());
160                                 }
161                         } else if (words[0].equalsIgnoreCase("cancel")) {
162                                 Integer index = Ints.tryParse(words[1]);
163                                 if ((index != null) && (index < downloads.size())) {
164                                         core.cancelDownload(downloads.get(index).bot(), downloads.get(index).pack());
165                                 }
166                         } else if (words[0].equalsIgnoreCase("stats")) {
167                                 int configuredChannelsCount = core.channels().size();
168                                 int joinedChannelsCount = core.joinedChannels().size();
169                                 int extraChannelsCount = core.extraChannels().size();
170                                 Collection<Bot> bots = core.bots();
171                                 Set<String> packNames = Sets.newHashSet();
172                                 int packsCount = 0;
173                                 for (Bot bot : bots) {
174                                         packsCount += bot.packs().size();
175                                         for (Pack pack : bot) {
176                                                 packNames.add(pack.name());
177                                         }
178                                 }
179
180                                 writeLine(String.format("%d channels (%d joined, %d extra), %d bots offering %d packs (%d unique).", configuredChannelsCount, joinedChannelsCount, extraChannelsCount, bots.size(), packsCount, packNames.size()));
181                         } else if (words[0].equalsIgnoreCase("connections")) {
182                                 lastConnections.clear();
183                                 int counter = 0;
184                                 for (Connection connection : core.connections()) {
185                                         lastConnections.add(connection);
186                                         writer.write(String.format("[%d] %s:%d, %s/s\n", counter++, connection.hostname(), connection.port(), f(connection.getInputRate())));
187                                 }
188                                 writeLine("End of connections.");
189                         } else if (words[0].equalsIgnoreCase("disconnect")) {
190                                 if ((words.length == 1) || ("all".equals(words[1]))) {
191                                         for (Connection connection : lastConnections) {
192                                                 core.closeConnection(connection);
193                                         }
194                                 } else {
195                                         Integer index = Ints.tryParse(words[1]);
196                                         if ((index != null) && (index < lastConnections.size())) {
197                                                 core.closeConnection(lastConnections.get(index));
198                                         }
199                                 }
200                         }
201
202                         lastLine = line;
203                 }
204         }
205
206         //
207         // EVENT HANDLERS
208         //
209
210         /**
211          * Called when a download was started.
212          *
213          * @param downloadStarted
214          *              The download started event
215          */
216         @Subscribe
217         public void downloadStarted(DownloadStarted downloadStarted) {
218                 Download download = downloadStarted.download();
219                 try {
220                         writeLine(String.format("Download of %s (from %s, %s) has started.", download.pack().name(), download.bot().name(), download.bot().network().name()));
221                 } catch (IOException ioe1) {
222                         /* ignore. */
223                 }
224         }
225
226         /**
227          * Called when a download is finished.
228          *
229          * @param downloadFinished
230          *              The download finished event
231          */
232         @Subscribe
233         public void downloadFinished(DownloadFinished downloadFinished) {
234                 Download download = downloadFinished.download();
235                 try {
236                         writeLine(String.format("Download of %s (from %s, %s) has finished, at %s/s.", download.pack().name(), download.bot().name(), download.bot().network().name(), f(download.dccReceiver().overallRate())));
237                 } catch (IOException ioe1) {
238                         /* ignore. */
239                 }
240         }
241
242         /**
243          * Called when a download fails.
244          *
245          * @param downloadFailed
246          *              The download failed event
247          */
248         @Subscribe
249         public void downloadFailed(DownloadFailed downloadFailed) {
250                 Download download = downloadFailed.download();
251                 try {
252                         writeLine(String.format("Download of %s (from %s, %s) has failed at %.1f%% and %s/s.", download.filename(), download.bot().name(), download.bot().network().name(), download.dccReceiver().progress() * 100.0 / download.dccReceiver().size(), f(download.dccReceiver().overallRate())));
253                 } catch (IOException ioe1) {
254                         /* ignore. */
255                 }
256         }
257
258         /**
259          * Displays the received message on the console.
260          *
261          * @param messageReceived
262          *              The message received event
263          */
264         @Subscribe
265         public void messageReceived(MessageReceived messageReceived) {
266                 try {
267                         writeLine(String.format("Message from %s: %s", messageReceived.source(), MessageCleaner.getDefaultInstance().clean(messageReceived.message())));
268                 } catch (IOException e) {
269                         /* ignore. */
270                 }
271         }
272
273         /**
274          * Writes a generic message to the console.
275          *
276          * @param genericMessage
277          *              The generic message event
278          */
279         @Subscribe
280         public void genericMessage(GenericMessage genericMessage) {
281                 try {
282                         writeLine(genericMessage.message());
283                 } catch (IOException ioe1) {
284                         /* ignore. */
285                 }
286         }
287
288         //
289         // PRIVATE METHODS
290         //
291
292         /**
293          * Writes the given line followed by an LF to the {@link #writer}.
294          *
295          * @param line
296          *              The line to write
297          * @throws IOException
298          *              if an I/O error occurs
299          */
300         private void writeLine(String line) throws IOException {
301                 writer.write(line + "\n");
302                 writer.flush();
303         }
304
305         /**
306          * Converts large numbers into a human-friendly format, by showing SI prefixes
307          * for ×1024 (K), ×1048576 (M), and ×1073741824 (G).
308          *
309          * @param number
310          *              The number to convert
311          * @return The converted number
312          */
313         private static String f(long number) {
314                 if (number >= (1 << 30)) {
315                         return String.format("%.1fG", number / (double) (1 << 30));
316                 }
317                 if (number >= (1 << 20)) {
318                         return String.format("%.1fM", number / (double) (1 << 20));
319                 }
320                 if (number >= (1 << 10)) {
321                         return String.format("%.1fK", number / (double) (1 << 10));
322                 }
323                 return String.format("%dB", number);
324         }
325
326         /**
327          * Returns the estimated time left for the given transfer.
328          *
329          * @param dccReceiver
330          *              The DCC receiver to get the time left for
331          * @return The time left for the transfer, or “unknown” if the time can not be
332          *         estimated
333          */
334         private static String getTimeLeft(DccReceiver dccReceiver) {
335                 if ((dccReceiver.size() == -1) || (dccReceiver.currentRate() == 0)) {
336                         return "unknown";
337                 }
338                 long secondsLeft = (dccReceiver.size() - dccReceiver.progress()) / dccReceiver.currentRate();
339                 if (secondsLeft > 3600) {
340                         return String.format("%02d:%02d:%02d", secondsLeft / 3600, (secondsLeft / 60) % 60, secondsLeft % 60);
341                 }
342                 return String.format("%02d:%02d", (secondsLeft / 60) % 60, secondsLeft % 60);
343         }
344
345         /** Container for result information. */
346         private static class Result implements Comparable<Result> {
347
348                 /** {@link Predicate} that matches {@link Result}s that contain an archive. */
349                 private static final Predicate<Result> isArchive = new Predicate<Result>() {
350
351                         /** All suffixes that are recognized as archives. */
352                         private final List<String> archiveSuffixes = Arrays.asList("rar", "tar", "zip", "tar.gz", "tar.bz2", "tar.lzma", "7z");
353
354                         @Override
355                         public boolean apply(Result result) {
356                                 for (String suffix : archiveSuffixes) {
357                                         if (result.pack().name().toLowerCase().endsWith(suffix)) {
358                                                 return true;
359                                         }
360                                 }
361                                 return false;
362                         }
363                 };
364
365                 /**
366                  * {@link Comparator} for {@link Result}s that sorts archives (as per {@link
367                  * #isArchive} to the back of the list.
368                  */
369                 private static final Comparator<Result> packArchiveComparator = new Comparator<Result>() {
370                         @Override
371                         public int compare(Result leftResult, Result rightResult) {
372                                 if (isArchive.apply(leftResult) && !isArchive.apply(rightResult)) {
373                                         return 1;
374                                 }
375                                 if (!isArchive.apply(leftResult) && isArchive.apply(rightResult)) {
376                                         return -1;
377                                 }
378                                 return 0;
379                         }
380                 };
381
382                 /**
383                  * {@link Comparator} for bot nicknames. It comprises different strategies:
384                  * one name pattern is preferred (and thus listed first), one pattern is
385                  * disliked (and thus listed last), the rest is sorted alphabetically.
386                  */
387                 private static final Comparator<Result> botNameComparator = new Comparator<Result>() {
388
389                         /** Regular expression pattern for preferred names. */
390                         private final Pattern preferredNames = Pattern.compile("(?i)[^\\w]EUR?[^\\w]");
391
392                         /** Regular expression pattern for disliked names. */
393                         private final Pattern dislikedNames = Pattern.compile("(?i)[^\\w]USA?[^\\w]");
394
395                         @Override
396                         public int compare(Result leftResult, Result rightResult) {
397                                 String leftBotName = leftResult.bot().name();
398                                 String rightBotName = rightResult.bot().name();
399                                 /* preferred names to the front! */
400                                 if (preferredNames.matcher(leftBotName).find() && !preferredNames.matcher(rightBotName).find()) {
401                                         return -1;
402                                 }
403                                 if (preferredNames.matcher(rightBotName).find() && !preferredNames.matcher(leftBotName).find()) {
404                                         return 1;
405                                 }
406                                 /* disliked names to the back. */
407                                 if (dislikedNames.matcher(leftBotName).find() && !dislikedNames.matcher(rightBotName).find()) {
408                                         return 1;
409                                 }
410                                 if (dislikedNames.matcher(rightBotName).find() && !dislikedNames.matcher(leftBotName).find()) {
411                                         return -1;
412                                 }
413                                 return 0;
414                         }
415                 };
416
417                 /**
418                  * {@link Comparator} for {@link Result}s that sorts them by the name of the
419                  * {@link Pack}.
420                  */
421                 private static final Comparator<Result> packNameComparator = new Comparator<Result>() {
422                         @Override
423                         public int compare(Result leftResult, Result rightResult) {
424                                 return leftResult.pack().name().compareToIgnoreCase(rightResult.pack().name());
425                         }
426                 };
427
428                 /** The bot carrying the pack. */
429                 private final Bot bot;
430
431                 /** The pack. */
432                 private final Pack pack;
433
434                 /**
435                  * Creates a new result.
436                  *
437                  * @param bot
438                  *              The bot carrying the pack
439                  * @param pack
440                  *              The pack
441                  */
442                 private Result(Bot bot, Pack pack) {
443                         this.bot = bot;
444                         this.pack = pack;
445                 }
446
447                 //
448                 // ACCESSORS
449                 //
450
451                 /**
452                  * Returns the bot carrying the pack.
453                  *
454                  * @return The bot carrying the pack
455                  */
456                 public Bot bot() {
457                         return bot;
458                 }
459
460                 /**
461                  * Returns the pack.
462                  *
463                  * @return The pack
464                  */
465                 public Pack pack() {
466                         return pack;
467                 }
468
469                 //
470                 // COMPARABLE METHODS
471                 //
472
473                 @Override
474                 public int compareTo(Result result) {
475                         return ComparisonChain.start()
476                                         .compare(this, result, packArchiveComparator)
477                                         .compare(this, result, botNameComparator)
478                                         .compare(this, result, packNameComparator).result();
479                 }
480
481         }
482
483 }