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