Show estimated time left on running downloads.
[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<Connection> lastConnections = Lists.newArrayList();
100                 while ((line = reader.readLine()) != null) {
101                         if (line.equals("")) {
102                                 line = lastLine;
103                         }
104                         String[] words = line.split(" +");
105                         if (words[0].equalsIgnoreCase("search")) {
106                                 lastResult.clear();
107                                 for (Bot bot : Lists.newArrayList(core.bots())) {
108                                         for (Pack pack : Lists.newArrayList(bot)) {
109                                                 boolean found = true;
110                                                 for (int wordIndex = 1; wordIndex < words.length; ++wordIndex) {
111                                                         if (words[wordIndex].startsWith("-") && pack.name().toLowerCase().contains(words[wordIndex].toLowerCase().substring(1))) {
112                                                                 found = false;
113                                                                 break;
114                                                         }
115                                                         if (!words[wordIndex].startsWith("-") && !pack.name().toLowerCase().contains(words[wordIndex].toLowerCase())) {
116                                                                 found = false;
117                                                                 break;
118                                                         }
119                                                 }
120                                                 if (found) {
121                                                         lastResult.add(new Result(bot, pack));
122                                                 }
123                                         }
124                                 }
125                                 Collections.sort(lastResult);
126                                 int counter = 0;
127                                 for (Result result : lastResult) {
128                                         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()));
129                                 }
130                                 writeLine("End of Search.");
131                         } else if (words[0].equalsIgnoreCase("dcc")) {
132                                 int counter = 0;
133                                 for (Download download : FluentIterable.from(core.downloads()).toSortedList(Ordering.from(BY_NAME).compound(BY_RUNNING))) {
134                                         DccReceiver dccReceiver = download.dccReceiver();
135                                         if (dccReceiver == null) {
136                                                 /* download has not even started. */
137                                                 writer.write(String.format("[%d] %s requested from %s (not started yet)\n", counter++, download.pack().name(), download.bot().name()));
138                                                 continue;
139                                         }
140                                         writer.write(String.format("[%d] %s from %s (%s, ", counter++, dccReceiver.filename(), download.bot().name(), f(dccReceiver.size())));
141                                         if (dccReceiver.isRunning()) {
142                                                 writer.write(String.format("%.1f%%, %s/s, %s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate()), getTimeLeft(dccReceiver)));
143                                         } else {
144                                                 if (dccReceiver.progress() >= dccReceiver.size()) {
145                                                         writer.write(String.format("complete, %s/s", f(dccReceiver.overallRate())));
146                                                 } else {
147                                                         writer.write(String.format("aborted at %.1f%%, %s/s", dccReceiver.progress() * 100.0 / dccReceiver.size(), f(dccReceiver.currentRate())));
148                                                 }
149                                         }
150                                         writer.write(")\n");
151                                 }
152                                 writeLine("End of DCCs.");
153                         } else if (words[0].equalsIgnoreCase("get")) {
154                                 Integer index = Ints.tryParse(words[1]);
155                                 if ((index != null) && (index < lastResult.size())) {
156                                         core.fetch(lastResult.get(index).bot(), lastResult.get(index).pack());
157                                 }
158                         } else if (words[0].equalsIgnoreCase("stats")) {
159                                 int configuredChannelsCount = core.channels().size();
160                                 int joinedChannelsCount = core.joinedChannels().size();
161                                 int extraChannelsCount = core.extraChannels().size();
162                                 Collection<Bot> bots = core.bots();
163                                 Set<String> packNames = Sets.newHashSet();
164                                 int packsCount = 0;
165                                 for (Bot bot : bots) {
166                                         packsCount += bot.packs().size();
167                                         for (Pack pack : bot) {
168                                                 packNames.add(pack.name());
169                                         }
170                                 }
171
172                                 writeLine(String.format("%d channels (%d joined, %d extra), %d bots offering %d packs (%d unique).", configuredChannelsCount, joinedChannelsCount, extraChannelsCount, bots.size(), packsCount, packNames.size()));
173                         } else if (words[0].equalsIgnoreCase("connections")) {
174                                 lastConnections.clear();
175                                 int counter = 0;
176                                 for (Connection connection : core.connections()) {
177                                         lastConnections.add(connection);
178                                         writer.write(String.format("[%d] %s:%d, %s/s\n", counter++, connection.hostname(), connection.port(), f(connection.getInputRate())));
179                                 }
180                                 writeLine("End of connections.");
181                         } else if (words[0].equalsIgnoreCase("disconnect")) {
182                                 if ((words.length == 1) || ("all".equals(words[1]))) {
183                                         for (Connection connection : lastConnections) {
184                                                 core.closeConnection(connection);
185                                         }
186                                 } else {
187                                         Integer index = Ints.tryParse(words[1]);
188                                         if ((index != null) && (index < lastConnections.size())) {
189                                                 core.closeConnection(lastConnections.get(index));
190                                         }
191                                 }
192                         }
193
194                         lastLine = line;
195                 }
196         }
197
198         //
199         // EVENT HANDLERS
200         //
201
202         /**
203          * Called when a download was started.
204          *
205          * @param downloadStarted
206          *              The download started event
207          */
208         @Subscribe
209         public void downloadStarted(DownloadStarted downloadStarted) {
210                 Download download = downloadStarted.download();
211                 try {
212                         writeLine(String.format("Download of %s (from %s, %s) has started.", download.pack().name(), download.bot().name(), download.bot().network().name()));
213                 } catch (IOException ioe1) {
214                         /* ignore. */
215                 }
216         }
217
218         /**
219          * Called when a download is finished.
220          *
221          * @param downloadFinished
222          *              The download finished event
223          */
224         @Subscribe
225         public void downloadFinished(DownloadFinished downloadFinished) {
226                 Download download = downloadFinished.download();
227                 try {
228                         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())));
229                 } catch (IOException ioe1) {
230                         /* ignore. */
231                 }
232         }
233
234         /**
235          * Called when a download fails.
236          *
237          * @param downloadFailed
238          *              The download failed event
239          */
240         @Subscribe
241         public void downloadFailed(DownloadFailed downloadFailed) {
242                 Download download = downloadFailed.download();
243                 try {
244                         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())));
245                 } catch (IOException ioe1) {
246                         /* ignore. */
247                 }
248         }
249
250         /**
251          * Displays the received message on the console.
252          *
253          * @param messageReceived
254          *              The message received event
255          */
256         @Subscribe
257         public void messageReceived(MessageReceived messageReceived) {
258                 try {
259                         writeLine(String.format("Message from %s: %s", messageReceived.source(), MessageCleaner.getDefaultInstance().clean(messageReceived.message())));
260                 } catch (IOException e) {
261                         /* ignore. */
262                 }
263         }
264
265         /**
266          * Writes a generic message to the console.
267          *
268          * @param genericMessage
269          *              The generic message event
270          */
271         @Subscribe
272         public void genericMessage(GenericMessage genericMessage) {
273                 try {
274                         writeLine(genericMessage.message());
275                 } catch (IOException ioe1) {
276                         /* ignore. */
277                 }
278         }
279
280         //
281         // PRIVATE METHODS
282         //
283
284         /**
285          * Writes the given line followed by an LF to the {@link #writer}.
286          *
287          * @param line
288          *              The line to write
289          * @throws IOException
290          *              if an I/O error occurs
291          */
292         private void writeLine(String line) throws IOException {
293                 writer.write(line + "\n");
294                 writer.flush();
295         }
296
297         /**
298          * Converts large numbers into a human-friendly format, by showing SI prefixes
299          * for ×1024 (K), ×1048576 (M), and ×1073741824 (G).
300          *
301          * @param number
302          *              The number to convert
303          * @return The converted number
304          */
305         private static String f(long number) {
306                 if (number >= (1 << 30)) {
307                         return String.format("%.1fG", number / (double) (1 << 30));
308                 }
309                 if (number >= (1 << 20)) {
310                         return String.format("%.1fM", number / (double) (1 << 20));
311                 }
312                 if (number >= (1 << 10)) {
313                         return String.format("%.1fK", number / (double) (1 << 10));
314                 }
315                 return String.format("%dB", number);
316         }
317
318         /**
319          * Returns the estimated time left for the given transfer.
320          *
321          * @param dccReceiver
322          *              The DCC receiver to get the time left for
323          * @return The time left for the transfer, or “unknown” if the time can not be
324          *         estimated
325          */
326         private static String getTimeLeft(DccReceiver dccReceiver) {
327                 if ((dccReceiver.size() == -1) || (dccReceiver.currentRate() == 0)) {
328                         return "unknown";
329                 }
330                 long secondsLeft = (dccReceiver.size() - dccReceiver.progress()) / dccReceiver.currentRate();
331                 if (secondsLeft > 3600) {
332                         return String.format("%02d:%02d:%02d", secondsLeft / 3600, (secondsLeft / 60) % 60, secondsLeft % 60);
333                 }
334                 return String.format("%02d:%02d", (secondsLeft / 60) % 60, secondsLeft % 60);
335         }
336
337         /** Container for result information. */
338         private static class Result implements Comparable<Result> {
339
340                 /** {@link Predicate} that matches {@link Result}s that contain an archive. */
341                 private static final Predicate<Result> isArchive = new Predicate<Result>() {
342
343                         /** All suffixes that are recognized as archives. */
344                         private final List<String> archiveSuffixes = Arrays.asList("rar", "tar", "zip", "tar.gz", "tar.bz2", "tar.lzma", "7z");
345
346                         @Override
347                         public boolean apply(Result result) {
348                                 for (String suffix : archiveSuffixes) {
349                                         if (result.pack().name().toLowerCase().endsWith(suffix)) {
350                                                 return true;
351                                         }
352                                 }
353                                 return false;
354                         }
355                 };
356
357                 /**
358                  * {@link Comparator} for {@link Result}s that sorts archives (as per {@link
359                  * #isArchive} to the back of the list.
360                  */
361                 private static final Comparator<Result> packArchiveComparator = new Comparator<Result>() {
362                         @Override
363                         public int compare(Result leftResult, Result rightResult) {
364                                 if (isArchive.apply(leftResult) && !isArchive.apply(rightResult)) {
365                                         return 1;
366                                 }
367                                 if (!isArchive.apply(leftResult) && isArchive.apply(rightResult)) {
368                                         return -1;
369                                 }
370                                 return 0;
371                         }
372                 };
373
374                 /**
375                  * {@link Comparator} for bot nicknames. It comprises different strategies:
376                  * one name pattern is preferred (and thus listed first), one pattern is
377                  * disliked (and thus listed last), the rest is sorted alphabetically.
378                  */
379                 private static final Comparator<Result> botNameComparator = new Comparator<Result>() {
380
381                         /** Regular expression pattern for preferred names. */
382                         private final Pattern preferredNames = Pattern.compile("(?i)[^\\w]EUR?[^\\w]");
383
384                         /** Regular expression pattern for disliked names. */
385                         private final Pattern dislikedNames = Pattern.compile("(?i)[^\\w]USA?[^\\w]");
386
387                         @Override
388                         public int compare(Result leftResult, Result rightResult) {
389                                 String leftBotName = leftResult.bot().name();
390                                 String rightBotName = rightResult.bot().name();
391                                 /* preferred names to the front! */
392                                 if (preferredNames.matcher(leftBotName).find() && !preferredNames.matcher(rightBotName).find()) {
393                                         return -1;
394                                 }
395                                 if (preferredNames.matcher(rightBotName).find() && !preferredNames.matcher(leftBotName).find()) {
396                                         return 1;
397                                 }
398                                 /* disliked names to the back. */
399                                 if (dislikedNames.matcher(leftBotName).find() && !dislikedNames.matcher(rightBotName).find()) {
400                                         return 1;
401                                 }
402                                 if (dislikedNames.matcher(rightBotName).find() && !dislikedNames.matcher(leftBotName).find()) {
403                                         return -1;
404                                 }
405                                 return 0;
406                         }
407                 };
408
409                 /**
410                  * {@link Comparator} for {@link Result}s that sorts them by the name of the
411                  * {@link Pack}.
412                  */
413                 private static final Comparator<Result> packNameComparator = new Comparator<Result>() {
414                         @Override
415                         public int compare(Result leftResult, Result rightResult) {
416                                 return leftResult.pack().name().compareToIgnoreCase(rightResult.pack().name());
417                         }
418                 };
419
420                 /** The bot carrying the pack. */
421                 private final Bot bot;
422
423                 /** The pack. */
424                 private final Pack pack;
425
426                 /**
427                  * Creates a new result.
428                  *
429                  * @param bot
430                  *              The bot carrying the pack
431                  * @param pack
432                  *              The pack
433                  */
434                 private Result(Bot bot, Pack pack) {
435                         this.bot = bot;
436                         this.pack = pack;
437                 }
438
439                 //
440                 // ACCESSORS
441                 //
442
443                 /**
444                  * Returns the bot carrying the pack.
445                  *
446                  * @return The bot carrying the pack
447                  */
448                 public Bot bot() {
449                         return bot;
450                 }
451
452                 /**
453                  * Returns the pack.
454                  *
455                  * @return The pack
456                  */
457                 public Pack pack() {
458                         return pack;
459                 }
460
461                 //
462                 // COMPARABLE METHODS
463                 //
464
465                 @Override
466                 public int compareTo(Result result) {
467                         return ComparisonChain.start()
468                                         .compare(this, result, packArchiveComparator)
469                                         .compare(this, result, botNameComparator)
470                                         .compare(this, result, packNameComparator).result();
471                 }
472
473         }
474
475 }