From 5d962b76adef88663cfa4acc093836c71fe9dd82 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Tue, 22 Sep 2020 01:12:14 +0200 Subject: [PATCH] =?utf8?q?=E2=99=BB=EF=B8=8F=20Move=20output=20generation?= =?utf8?q?=20to=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../java/net/pterodactylus/rhynodge/State.java | 7 + .../java/net/pterodactylus/rhynodge/Trigger.java | 11 -- .../rhynodge/engine/ReactionRunner.java | 2 +- .../rhynodge/filters/ComicSiteFilter.java | 5 +- .../filters/webpages/savoy/SavoyTicketsFilter.java | 3 - .../rhynodge/states/AbstractState.java | 33 ++++ .../pterodactylus/rhynodge/states/ComicState.java | 100 +++++++++- .../rhynodge/states/EpisodeState.java | 148 ++++++++++++++- .../pterodactylus/rhynodge/states/FailedState.java | 21 +++ .../pterodactylus/rhynodge/states/FileState.java | 8 + .../pterodactylus/rhynodge/states/HtmlState.java | 16 ++ .../pterodactylus/rhynodge/states/HttpState.java | 8 + .../pterodactylus/rhynodge/states/OutputState.java | 15 +- .../pterodactylus/rhynodge/states/StringState.java | 8 + .../rhynodge/states/TorrentState.java | 114 +++++++++++- .../rhynodge/triggers/AlwaysTrigger.java | 23 --- .../rhynodge/triggers/FileExistenceTrigger.java | 8 - .../triggers/FileStateModifiedTrigger.java | 8 - .../rhynodge/triggers/NewComicTrigger.java | 121 +----------- .../rhynodge/triggers/NewEpisodeTrigger.java | 206 ++------------------- .../rhynodge/triggers/NewTorrentTrigger.java | 140 +------------- .../rhynodge/webpages/weather/WeatherState.kt | 115 ++++++++++-- .../rhynodge/webpages/weather/WeatherTrigger.kt | 108 +---------- .../rhynodge/states/StateManagerTest.java | 27 ++- .../rhynodge/queries/FallbackQueryTest.kt | 10 +- .../rhynodge/triggers/AlwaysTriggerTest.kt | 18 -- .../webpages/weather/WeatherTriggerTest.kt | 92 --------- .../weather/wetterde/WetterDeFilterTest.kt | 4 +- 29 files changed, 626 insertions(+), 754 deletions(-) diff --git a/build.gradle b/build.gradle index 814add9..582c0ff 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,7 @@ dependencies { compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.1.2" compile group: "com.google.inject", name: "guice", version: "4.0" compile group: "org.jetbrains.kotlinx", name: "kotlinx-html-jvm", version: "0.7.1" + compile group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' testCompile group: "junit", name: "junit", version:"4.12" testCompile group: "org.hamcrest", name: "hamcrest-library", version:"1.3" diff --git a/src/main/java/net/pterodactylus/rhynodge/State.java b/src/main/java/net/pterodactylus/rhynodge/State.java index d549d69..bda5bf5 100644 --- a/src/main/java/net/pterodactylus/rhynodge/State.java +++ b/src/main/java/net/pterodactylus/rhynodge/State.java @@ -17,6 +17,10 @@ package net.pterodactylus.rhynodge; +import javax.annotation.Nonnull; + +import net.pterodactylus.rhynodge.output.Output; + /** * Defines the current state of a system. * @@ -76,4 +80,7 @@ public interface State { */ Throwable exception(); + @Nonnull + Output output(Reaction reaction); + } diff --git a/src/main/java/net/pterodactylus/rhynodge/Trigger.java b/src/main/java/net/pterodactylus/rhynodge/Trigger.java index e4024f2..9f43a46 100644 --- a/src/main/java/net/pterodactylus/rhynodge/Trigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/Trigger.java @@ -17,7 +17,6 @@ package net.pterodactylus.rhynodge; -import net.pterodactylus.rhynodge.output.Output; import net.pterodactylus.rhynodge.states.FileState; /** @@ -53,14 +52,4 @@ public interface Trigger { */ boolean triggers(); - /** - * Returns the output of this trigger. This will only return a meaningful - * value if {@link #triggers()} returns {@code true}. - * - * @param reaction - * The reaction being triggered - * @return The output of this trigger - */ - Output output(Reaction reaction); - } diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/ReactionRunner.java b/src/main/java/net/pterodactylus/rhynodge/engine/ReactionRunner.java index f4ae72e..a4006d8 100644 --- a/src/main/java/net/pterodactylus/rhynodge/engine/ReactionRunner.java +++ b/src/main/java/net/pterodactylus/rhynodge/engine/ReactionRunner.java @@ -64,7 +64,7 @@ public class ReactionRunner implements Runnable { reactionState.saveState(newState); if (trigger.triggers()) { logger.info(format("Trigger was hit for %s, executing action...", reaction.name())); - reaction.action().execute(trigger.output(reaction)); + reaction.action().execute(newState.output(reaction)); } logger.info(format("Reaction %s finished.", reaction.name())); } diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/ComicSiteFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/ComicSiteFilter.java index 818f711..6bc67cd 100644 --- a/src/main/java/net/pterodactylus/rhynodge/filters/ComicSiteFilter.java +++ b/src/main/java/net/pterodactylus/rhynodge/filters/ComicSiteFilter.java @@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkArgument; import java.net.URI; import java.net.URISyntaxException; +import java.util.Collections; import java.util.List; import net.pterodactylus.rhynodge.Filter; @@ -61,7 +62,6 @@ public abstract class ComicSiteFilter implements Filter { return new FailedState(); } - ComicState comicState = new ComicState(); Comic comic = new Comic(title.or("")); int imageCounter = 0; for (String imageUrl : imageUrls) { @@ -75,9 +75,8 @@ public abstract class ComicSiteFilter implements Filter { throw new IllegalStateException(String.format("Could not resolve image URL “%s” against base URL “%s”.", imageUrl, htmlState.uri()), use1); } } - comicState.add(comic); - return comicState; + return new ComicState(Collections.singletonList(comic)); } // diff --git a/src/main/java/net/pterodactylus/rhynodge/filters/webpages/savoy/SavoyTicketsFilter.java b/src/main/java/net/pterodactylus/rhynodge/filters/webpages/savoy/SavoyTicketsFilter.java index 14c6c22..c418a4c 100644 --- a/src/main/java/net/pterodactylus/rhynodge/filters/webpages/savoy/SavoyTicketsFilter.java +++ b/src/main/java/net/pterodactylus/rhynodge/filters/webpages/savoy/SavoyTicketsFilter.java @@ -19,9 +19,6 @@ import java.util.stream.Collectors; import net.pterodactylus.rhynodge.Filter; import net.pterodactylus.rhynodge.State; -import net.pterodactylus.rhynodge.filters.webpages.savoy.Movie; -import net.pterodactylus.rhynodge.filters.webpages.savoy.MovieExtractor; -import net.pterodactylus.rhynodge.filters.webpages.savoy.TicketLink; import net.pterodactylus.rhynodge.states.HtmlState; import net.pterodactylus.rhynodge.states.OutputState; diff --git a/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java b/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java index afb7dc2..081fbcc 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/AbstractState.java @@ -17,10 +17,18 @@ package net.pterodactylus.rhynodge.states; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; +import net.pterodactylus.rhynodge.output.DefaultOutput; +import net.pterodactylus.rhynodge.output.Output; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.google.common.escape.Escaper; +import com.google.common.html.HtmlEscapers; /** * Abstract implementation of a {@link State} that knows about the basic @@ -143,4 +151,29 @@ public abstract class AbstractState implements State { return exception; } + @Nonnull + @Override + public Output output(Reaction reaction) { + return new DefaultOutput(summary(reaction)) + .addText("text/plain", plainText()) + .addText("text/html", htmlText()); + } + + @Nonnull + protected String summary(Reaction reaction) { + return reaction.name(); + } + + @Nonnull + protected abstract String plainText(); + + @Nullable + protected String htmlText() { + //noinspection UnstableApiUsage + return "
" + htmlEscaper.escape(plainText()) + "
"; + } + + @SuppressWarnings("UnstableApiUsage") + private static final Escaper htmlEscaper = HtmlEscapers.htmlEscaper(); + } diff --git a/src/main/java/net/pterodactylus/rhynodge/states/ComicState.java b/src/main/java/net/pterodactylus/rhynodge/states/ComicState.java index 7e80686..e1eff9a 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/ComicState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/ComicState.java @@ -17,13 +17,25 @@ package net.pterodactylus.rhynodge.states; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.states.ComicState.Comic; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; + +import static java.lang.String.format; /** * {@link net.pterodactylus.rhynodge.State} that can store an arbitrary amout of @@ -35,6 +47,21 @@ public class ComicState extends AbstractState implements Iterable { @JsonProperty private final List comics = Lists.newArrayList(); + private final Set newComics = new HashSet<>(); + + @SuppressWarnings("unused") + // used for deserialization + private ComicState() { + } + + public ComicState(Collection allComics) { + this.comics.addAll(allComics); + } + + public ComicState(Collection allComics, Collection newComics) { + this(allComics); + this.newComics.addAll(newComics); + } @Override public boolean isEmpty() { @@ -45,11 +72,6 @@ public class ComicState extends AbstractState implements Iterable { return comics; } - public ComicState add(Comic comic) { - comics.add(comic); - return this; - } - @Override public Iterator iterator() { return comics.iterator(); @@ -57,7 +79,69 @@ public class ComicState extends AbstractState implements Iterable { @Override public String toString() { - return String.format("ComicState[comics=%s]", comics()); + return format("ComicState[comics=%s]", comics()); + } + + @Nonnull + @Override + protected String summary(Reaction reaction) { + return format("New Comic found for “%s!”", reaction.name()); + } + + @Nonnull + @Override + protected String plainText() { + StringBuilder text = new StringBuilder(); + + for (Comic newComic : newComics) { + text.append("Comic Found: ").append(newComic.title()).append("\n\n"); + for (Strip strip : newComic) { + text.append("Image: ").append(strip.imageUrl()).append("\n"); + if (!StringUtils.isBlank(strip.comment())) { + text.append("Comment: ").append(strip.comment()).append("\n"); + } + } + text.append("\n\n"); + } + + return text.toString(); + } + + @Nullable + @Override + protected String htmlText() { + StringBuilder html = new StringBuilder(); + html.append(""); + + for (Comic newComic : newComics) { + generateComicHtml(html, newComic); + } + + List latestComics = new ArrayList<>(comics()); + Collections.reverse(latestComics); + int comicCount = 0; + for (Comic comic : latestComics) { + if (newComics.contains(comic)) { + continue; + } + generateComicHtml(html, comic); + if (++comicCount == 7) { + break; + } + } + + return html.append("").toString(); + } + + private void generateComicHtml(StringBuilder html, Comic comic) { + html.append("

").append(StringEscapeUtils.escapeHtml4(comic.title())).append("

\n"); + for (Strip strip : comic) { + html.append("
\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
\n"); + html.append("
").append(StringEscapeUtils.escapeHtml4(strip.comment())).append("
\n"); + } } /** @@ -111,7 +195,7 @@ public class ComicState extends AbstractState implements Iterable { @Override public String toString() { - return String.format("Comic[title=%s,strips=%s]", title(), strips()); + return format("Comic[title=%s,strips=%s]", title(), strips()); } } @@ -158,7 +242,7 @@ public class ComicState extends AbstractState implements Iterable { @Override public String toString() { - return String.format("Strip[imageUrl=%s,comment=%s]", imageUrl(), comment()); + return format("Strip[imageUrl=%s,comment=%s]", imageUrl(), comment()); } } diff --git a/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java b/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java index 78ca478..e022541 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/EpisodeState.java @@ -20,9 +20,15 @@ package net.pterodactylus.rhynodge.states; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; import net.pterodactylus.rhynodge.filters.EpisodeFilter; import net.pterodactylus.rhynodge.states.EpisodeState.Episode; @@ -30,6 +36,10 @@ import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Function; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Ordering; +import org.apache.commons.lang3.StringEscapeUtils; /** * {@link State} implementation that stores episodes of TV shows, parsed via @@ -39,28 +49,38 @@ import com.google.common.base.Function; */ public class EpisodeState extends AbstractState implements Iterable { - /** The episodes found in the current request. */ + /** + * The episodes found in the current request. + */ @JsonProperty - private final List episodes = new ArrayList(); + private final List episodes = new ArrayList<>(); + private final Set newEpisodes = new HashSet<>(); + private final Set changedEpisodes = new HashSet<>(); + private final Set newTorrentFiles = new HashSet<>(); /** * No-arg constructor for deserialization. */ @SuppressWarnings("unused") private EpisodeState() { - this(Collections. emptySet()); } /** * Creates a new episode state. * - * @param episodes - * The episodes of the request + * @param episodes The episodes of the request */ public EpisodeState(Collection episodes) { this.episodes.addAll(episodes); } + public EpisodeState(Collection episodes, Collection newEpisodes, Collection changedEpisodes, Collection newTorreFiles) { + this(episodes); + this.newEpisodes.addAll(newEpisodes); + this.changedEpisodes.addAll(changedEpisodes); + this.newTorrentFiles.addAll(newTorreFiles); + } + // // ACCESSORS // @@ -79,6 +99,124 @@ public class EpisodeState extends AbstractState implements Iterable { return Collections.unmodifiableCollection(episodes); } + @Nonnull + @Override + protected String summary(Reaction reaction) { + if (!newEpisodes.isEmpty()) { + if (!changedEpisodes.isEmpty()) { + return String.format("%d new and %d changed Torrent(s) for “%s!”", newEpisodes.size(), changedEpisodes.size(), reaction.name()); + } + return String.format("%d new Torrent(s) for “%s!”", newEpisodes.size(), reaction.name()); + } + return String.format("%d changed Torrent(s) for “%s!”", changedEpisodes.size(), reaction.name()); + } + + @Nonnull + @Override + protected String plainText() { + StringBuilder stringBuilder = new StringBuilder(); + if (!newEpisodes.isEmpty()) { + stringBuilder.append("New Episodes\n\n"); + for (Episode episode : newEpisodes) { + stringBuilder.append("- ").append(episode.identifier()).append("\n"); + for (TorrentFile torrentFile : episode) { + stringBuilder.append(" - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n"); + if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { + stringBuilder.append(" Magnet: ").append(torrentFile.magnetUri()).append("\n"); + } + if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { + stringBuilder.append(" Download: ").append(torrentFile.downloadUri()).append("\n"); + } + } + } + } + if (!changedEpisodes.isEmpty()) { + stringBuilder.append("Changed Episodes\n\n"); + for (Episode episode : changedEpisodes) { + stringBuilder.append("- ").append(episode.identifier()).append("\n"); + for (TorrentFile torrentFile : episode) { + stringBuilder.append(" - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n"); + if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { + stringBuilder.append(" Magnet: ").append(torrentFile.magnetUri()).append("\n"); + } + if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { + stringBuilder.append(" Download: ").append(torrentFile.downloadUri()).append("\n"); + } + } + } + } + /* list all known episodes. */ + stringBuilder.append("All Known Episodes\n\n"); + ImmutableMap> episodesBySeason = FluentIterable.from(episodes).index(Episode::season).asMap(); + for (Map.Entry> seasonEntry : episodesBySeason.entrySet()) { + stringBuilder.append(" Season ").append(seasonEntry.getKey()).append("\n\n"); + for (Episode episode : Ordering.natural().sortedCopy(seasonEntry.getValue())) { + stringBuilder.append(" Episode ").append(episode.episode()).append("\n"); + for (TorrentFile torrentFile : episode) { + stringBuilder.append(" Size: ").append(torrentFile.size()); + stringBuilder.append(" in ").append(torrentFile.fileCount()).append(" file(s): "); + stringBuilder.append(torrentFile.magnetUri()); + } + } + } + return stringBuilder.toString(); + } + + @Nullable + @Override + protected String htmlText() { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append("\n"); + /* show all known episodes. */ + htmlBuilder.append("\n\n"); + htmlBuilder.append("\n"); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append("\n"); + htmlBuilder.append("\n"); + htmlBuilder.append("\n"); + Episode lastEpisode = null; + for (Map.Entry> seasonEntry : FluentIterable.from(Ordering.natural().reverse().sortedCopy(episodes)).index(Episode.BY_SEASON).asMap().entrySet()) { + for (Episode episode : seasonEntry.getValue()) { + for (TorrentFile torrentFile : episode) { + if (newEpisodes.contains(episode)) { + htmlBuilder.append(""); + } else if (newTorrentFiles.contains(torrentFile)) { + htmlBuilder.append(""); + } else { + htmlBuilder.append(""); + } + if ((lastEpisode == null) || !lastEpisode.equals(episode)) { + htmlBuilder.append(""); + } else { + htmlBuilder.append(""); + } + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append("\n"); + lastEpisode = episode; + } + } + } + htmlBuilder.append("\n"); + htmlBuilder.append("
All Known Episodes
SeasonEpisodeFilenameSizeFile(s)SeedsLeechersMagnetDownload
").append(episode.season()).append("").append(episode.episode()).append("").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("").append(torrentFile.fileCount()).append("").append(torrentFile.seedCount()).append("").append(torrentFile.leechCount()).append("LinkLink
\n"); + htmlBuilder.append("\n"); + return htmlBuilder.toString(); + } + // // ITERABLE INTERFACE // diff --git a/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java b/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java index a9dbba5..6c1d844 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/FailedState.java @@ -17,6 +17,12 @@ package net.pterodactylus.rhynodge.states; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import javax.annotation.Nonnull; + import net.pterodactylus.rhynodge.State; /** @@ -51,6 +57,21 @@ public class FailedState extends AbstractState { return true; } + @Nonnull + @Override + protected String plainText() { + if (exception() == null) { + return "Failed"; + } + try (Writer stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter)) { + exception().printStackTrace(printWriter); + return "Failed: " + stringWriter.toString(); + } catch (IOException ioe1) { + return "Failed while rendering exception"; + } + } + // // STATIC METHODS // diff --git a/src/main/java/net/pterodactylus/rhynodge/states/FileState.java b/src/main/java/net/pterodactylus/rhynodge/states/FileState.java index 0c7b4cb..983500d 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/FileState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/FileState.java @@ -17,6 +17,8 @@ package net.pterodactylus.rhynodge.states; +import javax.annotation.Nonnull; + import net.pterodactylus.rhynodge.State; /** @@ -119,6 +121,12 @@ public class FileState extends AbstractState { return modificationTime; } + @Nonnull + @Override + protected String plainText() { + return toString(); + } + // // OBJECT METHODS // diff --git a/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java b/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java index d2731f7..83cd36d 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/HtmlState.java @@ -17,6 +17,9 @@ package net.pterodactylus.rhynodge.states; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import net.pterodactylus.rhynodge.State; import org.jsoup.nodes.Document; @@ -74,6 +77,19 @@ public class HtmlState extends AbstractState { return document; } + @Nonnull + @Override + protected String plainText() { + //noinspection ConstantConditions + return htmlText(); + } + + @Nullable + @Override + protected String htmlText() { + return document.toString(); + } + // // OBJECT METHODS // diff --git a/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java b/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java index ea733f7..76dbc94 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/HttpState.java @@ -21,6 +21,8 @@ import static java.util.Arrays.copyOf; import java.io.UnsupportedEncodingException; +import javax.annotation.Nonnull; + import net.pterodactylus.rhynodge.State; import net.pterodactylus.rhynodge.queries.HttpQuery; @@ -126,6 +128,12 @@ public class HttpState extends AbstractState { } } + @Nonnull + @Override + protected String plainText() { + return content(); + } + // // STATIC METHODS // diff --git a/src/main/java/net/pterodactylus/rhynodge/states/OutputState.java b/src/main/java/net/pterodactylus/rhynodge/states/OutputState.java index 89d1fcb..f5d93f3 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/OutputState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/OutputState.java @@ -2,6 +2,9 @@ package net.pterodactylus.rhynodge.states; import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import net.pterodactylus.rhynodge.State; import com.fasterxml.jackson.annotation.JsonProperty; @@ -31,12 +34,16 @@ public class OutputState extends AbstractState { return !plainTextOutput.isPresent() && !htmlOutput.isPresent(); } - public Optional plainTextOutput() { - return plainTextOutput; + @Nonnull + @Override + protected String plainText() { + return plainTextOutput.orElse(""); } - public Optional htmlOutput() { - return htmlOutput; + @Nullable + @Override + protected String htmlText() { + return htmlOutput.orElse(null); } } diff --git a/src/main/java/net/pterodactylus/rhynodge/states/StringState.java b/src/main/java/net/pterodactylus/rhynodge/states/StringState.java index 12c21a7..ad989b3 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/StringState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/StringState.java @@ -17,6 +17,8 @@ package net.pterodactylus.rhynodge.states; +import javax.annotation.Nonnull; + /** * A {@link net.pterodactylus.rhynodge.State} that stores a single {@link * String} value. @@ -52,4 +54,10 @@ public class StringState extends AbstractState { return value.isEmpty(); } + @Nonnull + @Override + protected String plainText() { + return value; + } + } diff --git a/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java b/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java index 3a7bf54..f2e9ae2 100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java @@ -20,18 +20,31 @@ package net.pterodactylus.rhynodge.states; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.Set; +import javax.annotation.Nonnull; + +import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; +import net.pterodactylus.rhynodge.output.DefaultOutput; +import net.pterodactylus.rhynodge.output.Output; import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import org.apache.commons.lang3.StringEscapeUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.collect.Lists; +import static com.google.common.collect.Ordering.from; +import static java.lang.String.format; /** * {@link State} that contains information about an arbitrary number of torrent @@ -45,11 +58,13 @@ public class TorrentState extends AbstractState implements Iterable @JsonProperty private List files = Lists.newArrayList(); + private final Set newTorrentFiles = new HashSet<>(); + /** * Creates a new torrent state without torrent files. */ public TorrentState() { - this(Collections. emptySet()); + this(Collections.emptySet()); } /** @@ -62,6 +77,11 @@ public class TorrentState extends AbstractState implements Iterable files.addAll(torrentFiles); } + public TorrentState(Collection torrentFiles, Collection newTorrentFiles) { + files.addAll(torrentFiles); + this.newTorrentFiles.addAll(newTorrentFiles); + } + // // ACCESSORS // @@ -92,6 +112,90 @@ public class TorrentState extends AbstractState implements Iterable return this; } + @Nonnull + @Override + protected String summary(Reaction reaction) { + return format("Found %d new Torrent(s) for “%s!”", newTorrentFiles.size(), reaction.name()); + } + + @Nonnull + @Override + protected String plainText() { + StringBuilder plainText = new StringBuilder(); + plainText.append("New Torrents:\n\n"); + for (TorrentFile torrentFile : newTorrentFiles) { + plainText.append(torrentFile.name()).append('\n'); + plainText.append('\t').append(torrentFile.size()).append(" in ").append(torrentFile.fileCount()).append(" file(s)\n"); + plainText.append('\t').append(torrentFile.seedCount()).append(" seed(s), ").append(torrentFile.leechCount()).append(" leecher(s)\n"); + if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { + plainText.append('\t').append(torrentFile.magnetUri()).append('\n'); + } + if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { + plainText.append('\t').append(torrentFile.downloadUri()).append('\n'); + } + plainText.append('\n'); + } + return plainText.toString(); + } + + @Nullable + @Override + protected String htmlText() { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append("\n"); + htmlBuilder.append("\n\n"); + htmlBuilder.append("\n"); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append("\n"); + htmlBuilder.append("\n"); + htmlBuilder.append("\n"); + for (TorrentFile torrentFile : sortNewFirst().sortedCopy(files)) { + if (newTorrentFiles.contains(torrentFile)) { + htmlBuilder.append(""); + } else { + htmlBuilder.append(""); + } + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append(""); + htmlBuilder.append("\n"); + } + htmlBuilder.append("\n"); + htmlBuilder.append("
All Known Torrents
FilenameSizeFile(s)SeedsLeechersMagnetDownload
").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("").append(torrentFile.fileCount()).append("").append(torrentFile.seedCount()).append("").append(torrentFile.leechCount()).append("LinkLink
\n"); + htmlBuilder.append("\n"); + return htmlBuilder.toString(); + } + + /** + * Returns an ordering that sorts torrent files by whether they are new + * (according to {@link #files}) or not. New files will be sorted + * first. + * + * @return An ordering for “new files first” + */ + private Ordering sortNewFirst() { + return from((TorrentFile leftTorrentFile, TorrentFile rightTorrentFile) -> { + if (newTorrentFiles.contains(leftTorrentFile) && !newTorrentFiles.contains(rightTorrentFile)) { + return -1; + } + if (!newTorrentFiles.contains(leftTorrentFile) && newTorrentFiles.contains(rightTorrentFile)) { + return 1; + } + return 0; + }); + } + // // ITERABLE METHODS // @@ -113,7 +217,7 @@ public class TorrentState extends AbstractState implements Iterable */ @Override public String toString() { - return String.format("%s[files=%s]", getClass().getSimpleName(), files); + return format("%s[files=%s]", getClass().getSimpleName(), files); } /** @@ -332,7 +436,7 @@ public class TorrentState extends AbstractState implements Iterable */ @Override public String toString() { - return String.format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri()); + return format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri()); } } diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java index b916237..0018f50 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/AlwaysTrigger.java @@ -17,12 +17,8 @@ package net.pterodactylus.rhynodge.triggers; -import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; import net.pterodactylus.rhynodge.Trigger; -import net.pterodactylus.rhynodge.output.DefaultOutput; -import net.pterodactylus.rhynodge.output.Output; -import net.pterodactylus.rhynodge.states.OutputState; /** * {@link Trigger} implementation that always triggers. @@ -54,23 +50,4 @@ public class AlwaysTrigger implements Trigger { return true; } - /** - * {@inheritDoc} - */ - @Override - public Output output(Reaction reaction) { - DefaultOutput output = new DefaultOutput(reaction.name()); - if (currentState instanceof OutputState) { - OutputState outputState = (OutputState) currentState; - if (outputState.plainTextOutput().isPresent()) { - output = output.addText("text/plain", outputState.plainTextOutput().get()); - } - if (outputState.htmlOutput().isPresent()) { - output = output.addText("text/html", outputState.htmlOutput().get()); - } - return output; - } - return output.addText("text/plain", "true").addText("text/html", "
true
"); - } - } diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java index 264740f..8681871 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/FileExistenceTrigger.java @@ -59,12 +59,4 @@ public class FileExistenceTrigger implements Trigger { return triggered; } - /** - * {@inheritDoc} - */ - @Override - public Output output(Reaction reaction) { - return new DefaultOutput("File appeared/disappeared").addText("text/plain", "File appeared/disappeared").addText("text/html", "
File appeared/disappeared
"); - } - } diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java index 7b3026c..f1f114f 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/FileStateModifiedTrigger.java @@ -58,12 +58,4 @@ public class FileStateModifiedTrigger implements Trigger { return triggered; } - /** - * {@inheritDoc} - */ - @Override - public Output output(Reaction reaction) { - return new DefaultOutput("File modified").addText("text/plain", "File modified").addText("text/html", "
File modified
"); - } - } diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewComicTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewComicTrigger.java index d2e0688..416b9e2 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/NewComicTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewComicTrigger.java @@ -17,24 +17,15 @@ package net.pterodactylus.rhynodge.triggers; -import static com.google.common.base.Preconditions.*; +import java.util.HashSet; +import java.util.Set; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; import net.pterodactylus.rhynodge.Trigger; -import net.pterodactylus.rhynodge.output.DefaultOutput; -import net.pterodactylus.rhynodge.output.Output; import net.pterodactylus.rhynodge.states.ComicState; import net.pterodactylus.rhynodge.states.ComicState.Comic; -import net.pterodactylus.rhynodge.states.ComicState.Strip; -import com.google.common.collect.Lists; -import org.apache.commons.lang3.StringEscapeUtils; -import org.apache.commons.lang3.StringUtils; +import static com.google.common.base.Preconditions.checkArgument; /** * {@link Trigger} implementation that detects the presence of new {@link @@ -44,11 +35,7 @@ import org.apache.commons.lang3.StringUtils; */ public class NewComicTrigger implements Trigger { - /** The new comics. */ - private final List newComics = Lists.newArrayList(); - - /** The latest comic state. */ - private ComicState mergedComicState; + private boolean triggered = false; @Override public State mergeStates(State previousState, State currentState) { @@ -58,110 +45,22 @@ public class NewComicTrigger implements Trigger { ComicState previousComicState = (ComicState) previousState; ComicState currentComicState = (ComicState) currentState; - /* copy old state into new state. */ - mergedComicState = new ComicState(); - for (Comic comic : previousComicState) { - mergedComicState.add(comic); - } + Set allComics = new HashSet<>(previousComicState.comics()); + Set newComics = new HashSet<>(); - newComics.clear(); for (Comic comic : currentComicState) { - if (!mergedComicState.comics().contains(comic)) { + if (allComics.add(comic)) { newComics.add(comic); - mergedComicState.add(comic); + triggered = true; } } - return mergedComicState; + return new ComicState(allComics, newComics); } @Override public boolean triggers() { - return !newComics.isEmpty(); - } - - @Override - public Output output(Reaction reaction) { - DefaultOutput output = new DefaultOutput(String.format("New Comic found for “%s!”", reaction.name())); - - output.addText("text/plain", generatePlainText()); - output.addText("text/html", generateHtmlText()); - - return output; - } - - // - // PRIVATE METHODS - // - - /** - * Generates a list of the new comics in plain text format. - * - * @return The list of new comics in plain text format - */ - private String generatePlainText() { - StringBuilder text = new StringBuilder(); - - for (Comic newComic : newComics) { - text.append("Comic Found: ").append(newComic.title()).append("\n\n"); - for (Strip strip : newComic) { - text.append("Image: ").append(strip.imageUrl()).append("\n"); - if (!StringUtils.isBlank(strip.comment())) { - text.append("Comment: ").append(strip.comment()).append("\n"); - } - } - text.append("\n\n"); - } - - return text.toString(); - } - - /** - * Generates a list of new comics in HTML format. - * - * @return The list of new comics in HTML format - */ - private String generateHtmlText() { - StringBuilder html = new StringBuilder(); - html.append(""); - - for (Comic newComic : newComics) { - generateComicHtml(html, newComic); - } - - List latestComics = new ArrayList(mergedComicState.comics()); - Collections.reverse(latestComics); - int comicCount = 0; - for (Comic comic : latestComics) { - if (newComics.contains(comic)) { - continue; - } - generateComicHtml(html, comic); - if (++comicCount == 7) { - break; - } - } - - return html.append("").toString(); - } - - /** - * Generates the HTML for a single comic. - * - * @param html - * The string builder to append the HTML to - * @param comic - * The comic to render - */ - private void generateComicHtml(StringBuilder html, Comic comic) { - html.append("

").append(StringEscapeUtils.escapeHtml4(comic.title())).append("

\n"); - for (Strip strip : comic) { - html.append("
\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
\n"); - html.append("
").append(StringEscapeUtils.escapeHtml4(strip.comment())).append("
\n"); - } + return triggered; } } diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java index ea15c87..4b1d209 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java @@ -17,30 +17,20 @@ package net.pterodactylus.rhynodge.triggers; -import static com.google.common.base.Preconditions.checkState; - +import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.Map; -import java.util.Map.Entry; -import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; import net.pterodactylus.rhynodge.Trigger; -import net.pterodactylus.rhynodge.output.DefaultOutput; -import net.pterodactylus.rhynodge.output.Output; import net.pterodactylus.rhynodge.states.EpisodeState; import net.pterodactylus.rhynodge.states.EpisodeState.Episode; import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile; -import org.apache.commons.lang3.StringEscapeUtils; - -import com.google.common.base.Function; -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Ordering; -import com.google.common.collect.Sets; +import static com.google.common.base.Preconditions.checkState; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; /** * {@link Trigger} implementation that compares two {@link EpisodeState}s for @@ -50,21 +40,7 @@ import com.google.common.collect.Sets; */ public class NewEpisodeTrigger implements Trigger { - /** All episodes. */ - private final Collection allEpisodes = Sets.newHashSet(); - - /** All new episodes. */ - private final Collection newEpisodes = Sets.newHashSet(); - - /** All changed episodes. */ - private final Collection changedEpisodes = Sets.newHashSet(); - - /** All new torrent files. */ - private final Collection newTorrentFiles = Sets.newHashSet(); - - // - // TRIGGER METHODS - // + private boolean triggered = false; /** * {@inheritDoc} @@ -73,24 +49,19 @@ public class NewEpisodeTrigger implements Trigger { public State mergeStates(State previousState, State currentState) { checkState(currentState instanceof EpisodeState, "currentState is not a EpisodeState but a %s", currentState.getClass().getName()); checkState(previousState instanceof EpisodeState, "previousState is not a EpisodeState but a %s", currentState.getClass().getName()); - newEpisodes.clear(); - changedEpisodes.clear(); - this.allEpisodes.clear(); - newTorrentFiles.clear(); - Map allEpisodes = Maps.newHashMap(FluentIterable.from(((EpisodeState) previousState).episodes()).toMap(new Function() { - @Override - public Episode apply(Episode episode) { - return episode; - } - })); + Collection newEpisodes = new HashSet<>(); + Collection changedEpisodes = new HashSet<>(); + Collection newTorrentFiles = new HashSet<>(); + Map allEpisodes = ((EpisodeState) previousState).episodes().stream().collect(toMap(identity(), identity())); for (Episode episode : ((EpisodeState) currentState).episodes()) { if (!allEpisodes.containsKey(episode)) { allEpisodes.put(episode, episode); newEpisodes.add(episode); + triggered = true; } Episode existingEpisode = allEpisodes.get(episode); - for (TorrentFile torrentFile : Lists.newArrayList(episode.torrentFiles())) { + for (TorrentFile torrentFile : new ArrayList<>(episode.torrentFiles())) { int oldSize = existingEpisode.torrentFiles().size(); existingEpisode.addTorrentFile(torrentFile); int newSize = existingEpisode.torrentFiles().size(); @@ -102,8 +73,7 @@ public class NewEpisodeTrigger implements Trigger { } } } - this.allEpisodes.addAll(allEpisodes.values()); - return new EpisodeState(this.allEpisodes); + return new EpisodeState(allEpisodes.values(), newEpisodes, changedEpisodes, newTorrentFiles); } /** @@ -111,155 +81,7 @@ public class NewEpisodeTrigger implements Trigger { */ @Override public boolean triggers() { - return !newEpisodes.isEmpty() || !changedEpisodes.isEmpty(); - } - - /** - * {@inheritDoc} - */ - @Override - public Output output(Reaction reaction) { - String summary; - if (!newEpisodes.isEmpty()) { - if (!changedEpisodes.isEmpty()) { - summary = String.format("%d new and %d changed Torrent(s) for “%s!”", newEpisodes.size(), changedEpisodes.size(), reaction.name()); - } else { - summary = String.format("%d new Torrent(s) for “%s!”", newEpisodes.size(), reaction.name()); - } - } else { - summary = String.format("%d changed Torrent(s) for “%s!”", changedEpisodes.size(), reaction.name()); - } - DefaultOutput output = new DefaultOutput(summary); - output.addText("text/plain", generatePlainText(reaction)); - output.addText("text/html", generateHtmlText(reaction)); - return output; - } - - // - // PRIVATE METHODS - // - - /** - * Generates the plain text trigger output. - * - * @param reaction - * The reaction that was triggered - * @return The plain text output - */ - private String generatePlainText(Reaction reaction) { - StringBuilder stringBuilder = new StringBuilder(); - if (!newEpisodes.isEmpty()) { - stringBuilder.append(reaction.name()).append(" - New Episodes\n\n"); - for (Episode episode : newEpisodes) { - stringBuilder.append("- ").append(episode.identifier()).append("\n"); - for (TorrentFile torrentFile : episode) { - stringBuilder.append(" - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n"); - if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { - stringBuilder.append(" Magnet: ").append(torrentFile.magnetUri()).append("\n"); - } - if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { - stringBuilder.append(" Download: ").append(torrentFile.downloadUri()).append("\n"); - } - } - } - } - if (!changedEpisodes.isEmpty()) { - stringBuilder.append(reaction.name()).append(" - Changed Episodes\n\n"); - for (Episode episode : changedEpisodes) { - stringBuilder.append("- ").append(episode.identifier()).append("\n"); - for (TorrentFile torrentFile : episode) { - stringBuilder.append(" - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n"); - if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { - stringBuilder.append(" Magnet: ").append(torrentFile.magnetUri()).append("\n"); - } - if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { - stringBuilder.append(" Download: ").append(torrentFile.downloadUri()).append("\n"); - } - } - } - } - /* list all known episodes. */ - stringBuilder.append(reaction.name()).append(" - All Known Episodes\n\n"); - ImmutableMap> episodesBySeason = FluentIterable.from(allEpisodes).index(new Function() { - - @Override - public Integer apply(Episode episode) { - return episode.season(); - } - }).asMap(); - for (Entry> seasonEntry : episodesBySeason.entrySet()) { - stringBuilder.append(" Season ").append(seasonEntry.getKey()).append("\n\n"); - for (Episode episode : Ordering.natural().sortedCopy(seasonEntry.getValue())) { - stringBuilder.append(" Episode ").append(episode.episode()).append("\n"); - for (TorrentFile torrentFile : episode) { - stringBuilder.append(" Size: ").append(torrentFile.size()); - stringBuilder.append(" in ").append(torrentFile.fileCount()).append(" file(s): "); - stringBuilder.append(torrentFile.magnetUri()); - } - } - } - - return stringBuilder.toString(); - } - - /** - * Generates the HTML trigger output. - * - * @param reaction - * The reaction that was triggered - * @return The HTML output - */ - private String generateHtmlText(Reaction reaction) { - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder.append("\n"); - /* show all known episodes. */ - htmlBuilder.append("\n\n"); - htmlBuilder.append("\n"); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append("\n"); - htmlBuilder.append("\n"); - htmlBuilder.append("\n"); - Episode lastEpisode = null; - for (Entry> seasonEntry : FluentIterable.from(Ordering.natural().reverse().sortedCopy(allEpisodes)).index(Episode.BY_SEASON).asMap().entrySet()) { - for (Episode episode : seasonEntry.getValue()) { - for (TorrentFile torrentFile : episode) { - if (newEpisodes.contains(episode)) { - htmlBuilder.append(""); - } else if (newTorrentFiles.contains(torrentFile)) { - htmlBuilder.append(""); - } else { - htmlBuilder.append(""); - } - if ((lastEpisode == null) || !lastEpisode.equals(episode)) { - htmlBuilder.append(""); - } else { - htmlBuilder.append(""); - } - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append("\n"); - lastEpisode = episode; - } - } - } - htmlBuilder.append("\n"); - htmlBuilder.append("
All Known Episodes
SeasonEpisodeFilenameSizeFile(s)SeedsLeechersMagnetDownload
").append(episode.season()).append("").append(episode.episode()).append("").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("").append(torrentFile.fileCount()).append("").append(torrentFile.seedCount()).append("").append(torrentFile.leechCount()).append("LinkLink
\n"); - htmlBuilder.append("\n"); - return htmlBuilder.toString(); + return triggered; } } diff --git a/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java index 7b28fee..0ff193c 100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java @@ -17,24 +17,15 @@ package net.pterodactylus.rhynodge.triggers; -import static com.google.common.base.Preconditions.checkState; - -import java.util.List; +import java.util.HashSet; import java.util.Set; -import net.pterodactylus.rhynodge.Reaction; import net.pterodactylus.rhynodge.State; import net.pterodactylus.rhynodge.Trigger; -import net.pterodactylus.rhynodge.output.DefaultOutput; -import net.pterodactylus.rhynodge.output.Output; import net.pterodactylus.rhynodge.states.TorrentState; import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile; -import org.apache.commons.lang3.StringEscapeUtils; - -import com.google.common.collect.Lists; -import com.google.common.collect.Ordering; -import com.google.common.collect.Sets; +import static com.google.common.base.Preconditions.checkState; /** * {@link Trigger} implementation that is triggered by {@link TorrentFile}s that @@ -44,11 +35,7 @@ import com.google.common.collect.Sets; */ public class NewTorrentTrigger implements Trigger { - /** All known torrents. */ - private final Set allTorrentFiles = Sets.newHashSet(); - - /** The newly detected torrent files. */ - private final List newTorrentFiles = Lists.newArrayList(); + private boolean triggered = false; // // TRIGGER METHODS @@ -61,19 +48,17 @@ public class NewTorrentTrigger implements Trigger { public State mergeStates(State previousState, State currentState) { checkState(currentState instanceof TorrentState, "currentState is not a TorrentState but a %s", currentState.getClass().getName()); checkState(previousState instanceof TorrentState, "previousState is not a TorrentState but a %s", currentState.getClass().getName()); - TorrentState currentTorrentState = (TorrentState) currentState; - TorrentState previousTorrentState = (TorrentState) previousState; - allTorrentFiles.clear(); - newTorrentFiles.clear(); - allTorrentFiles.addAll(previousTorrentState.torrentFiles()); - for (TorrentFile torrentFile : currentTorrentState) { + Set allTorrentFiles = new HashSet<>(((TorrentState) previousState).torrentFiles()); + Set newTorrentFiles = new HashSet<>(); + for (TorrentFile torrentFile : (TorrentState) currentState) { if (allTorrentFiles.add(torrentFile)) { newTorrentFiles.add(torrentFile); + triggered = true; } } - return new TorrentState(allTorrentFiles); + return new TorrentState(allTorrentFiles, newTorrentFiles); } /** @@ -81,114 +66,7 @@ public class NewTorrentTrigger implements Trigger { */ @Override public boolean triggers() { - return !newTorrentFiles.isEmpty(); - } - - /** - * {@inheritDoc} - */ - @Override - public Output output(Reaction reaction) { - DefaultOutput output = new DefaultOutput(String.format("Found %d new Torrent(s) for “%s!”", newTorrentFiles.size(), reaction.name())); - output.addText("text/plain", getPlainTextList(reaction)); - output.addText("text/html", getHtmlTextList(reaction)); - return output; - } - - // - // PRIVATE METHODS - // - - /** - * Generates a plain text list of torrent files. - * - * @param reaction - * The reaction that was triggered - * @return The generated plain text - */ - private String getPlainTextList(Reaction reaction) { - StringBuilder plainText = new StringBuilder(); - plainText.append("New Torrents:\n\n"); - for (TorrentFile torrentFile : newTorrentFiles) { - plainText.append(torrentFile.name()).append('\n'); - plainText.append('\t').append(torrentFile.size()).append(" in ").append(torrentFile.fileCount()).append(" file(s)\n"); - plainText.append('\t').append(torrentFile.seedCount()).append(" seed(s), ").append(torrentFile.leechCount()).append(" leecher(s)\n"); - if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { - plainText.append('\t').append(torrentFile.magnetUri()).append('\n'); - } - if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { - plainText.append('\t').append(torrentFile.downloadUri()).append('\n'); - } - plainText.append('\n'); - } - return plainText.toString(); - } - - /** - * Generates an HTML list of the given torrent files. - * - * @param reaction - * The reaction that was triggered - * @return The generated HTML - */ - private String getHtmlTextList(Reaction reaction) { - StringBuilder htmlBuilder = new StringBuilder(); - htmlBuilder.append("\n"); - htmlBuilder.append("\n\n"); - htmlBuilder.append("\n"); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append("\n"); - htmlBuilder.append("\n"); - htmlBuilder.append("\n"); - for (TorrentFile torrentFile : sortNewFirst().sortedCopy(allTorrentFiles)) { - if (newTorrentFiles.contains(torrentFile)) { - htmlBuilder.append(""); - } else { - htmlBuilder.append(""); - } - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append(""); - htmlBuilder.append("\n"); - } - htmlBuilder.append("\n"); - htmlBuilder.append("
All Known Torrents
FilenameSizeFile(s)SeedsLeechersMagnetDownload
").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("").append(torrentFile.fileCount()).append("").append(torrentFile.seedCount()).append("").append(torrentFile.leechCount()).append("LinkLink
\n"); - htmlBuilder.append("\n"); - return htmlBuilder.toString(); - } - - /** - * Returns an ordering that sorts torrent files by whether they are new - * (according to {@link #newTorrentFiles}) or not. New files will be sorted - * first. - * - * @return An ordering for “new files first” - */ - private Ordering sortNewFirst() { - return new Ordering() { - - @Override - public int compare(TorrentFile leftTorrentFile, TorrentFile rightTorrentFile) { - if (newTorrentFiles.contains(leftTorrentFile) && !newTorrentFiles.contains(rightTorrentFile)) { - return -1; - } - if (!newTorrentFiles.contains(leftTorrentFile) && newTorrentFiles.contains(rightTorrentFile)) { - return 1; - } - return 0; - } - }; + return triggered; } } diff --git a/src/main/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherState.kt b/src/main/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherState.kt index 0d1d7f1..814af33 100644 --- a/src/main/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherState.kt +++ b/src/main/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherState.kt @@ -2,10 +2,22 @@ package net.pterodactylus.rhynodge.webpages.weather import com.fasterxml.jackson.annotation.JsonGetter import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.html.body +import kotlinx.html.div +import kotlinx.html.h1 +import kotlinx.html.head +import kotlinx.html.html +import kotlinx.html.img +import kotlinx.html.stream.createHTML +import kotlinx.html.style +import kotlinx.html.unsafe import net.pterodactylus.rhynodge.states.AbstractState +import java.text.DateFormat import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.util.Locale /** * Contains a weather state. @@ -14,26 +26,97 @@ import java.time.ZonedDateTime */ class WeatherState(val service: String, val dateTime: ZonedDateTime) : AbstractState(true), Iterable { - constructor(@JsonProperty("service") service: String, @JsonProperty("dateTime") time: Long) : - this(service, Instant.ofEpochMilli(time).atZone(ZoneId.of("Europe/Berlin"))) + constructor(@JsonProperty("service") service: String, @JsonProperty("dateTime") time: Long) : + this(service, Instant.ofEpochMilli(time).atZone(ZoneId.of("Europe/Berlin"))) - @JsonProperty("hours") - val hours: List = mutableListOf() + @JsonProperty("hours") + val hours: List = mutableListOf() - @get:JsonGetter("dateTime") - val timeMillis = dateTime.toInstant().toEpochMilli() + @get:JsonGetter("dateTime") + val timeMillis = dateTime.toInstant().toEpochMilli() - operator fun plusAssign(hourState: HourState) { - (hours as MutableList).add(hourState) - } + operator fun plusAssign(hourState: HourState) { + (hours as MutableList).add(hourState) + } - override fun iterator(): Iterator { - return hours.iterator() - } + override fun iterator(): Iterator { + return hours.iterator() + } - override fun equals(other: Any?): Boolean { - other as? WeatherState ?: return false - return (dateTime == other.dateTime) and (hours == other.hours) - } + override fun equals(other: Any?): Boolean { + other as? WeatherState ?: return false + return (dateTime == other.dateTime) and (hours == other.hours) + } + + override fun plainText(): String="" + + override fun htmlText(): String = + createHTML().html { + head { + style("text/css") { + unsafe { + +".weather-states { display: table; } " + +".hour-state, .header { display: table-row; } " + +".hour-state > div { display: table-cell; padding: 0em 0.5em; text-align: center; } " + +".header > div { display: table-cell; padding: 0em 0.5em; font-weight: bold; text-align: center; } " + } + } + } + body { + val startTime = dateTime.toInstant() + h1 { +"The Weather (according to wetter.de) on %s".format(dateFormatter.format(startTime.toEpochMilli())) } + val showFeltTemperature = any { it.feltTemperature != null } + val showGustSpeed = any { it.gustSpeed != null } + val showHumidity = any { it.humidity != null } + div("weather-states") { + div("header") { + div { +"Time" } + div { +"Temperature" } + if (showHumidity) { + div { +"feels like" } + } + div { +"Chance of Rain" } + div { +"Amount" } + div { +"Wind from" } + div { +"Speed" } + if (showGustSpeed) { + div { +"Gusts" } + } + if (showHumidity) { + div { +"Humidity" } + } + div { +"Description" } + div { +"Image" } + } + forEach { + div("hour-state") { + div("time") { +"%tH:% div { display: table-cell; padding: 0em 0.5em; text-align: center; } " - +".header > div { display: table-cell; padding: 0em 0.5em; font-weight: bold; text-align: center; } " - } - } - } - body { - val startTime = state.dateTime.toInstant() - h1 { +"The Weather (according to wetter.de) on %s".format(dateFormatter.format(startTime.toEpochMilli())) } - val showFeltTemperature = state.any { it.feltTemperature != null } - val showGustSpeed = state.any { it.gustSpeed != null } - val showHumidity = state.any { it.humidity != null } - div("weather-states") { - div("header") { - div { +"Time" } - div { +"Temperature" } - if (showHumidity) { - div { +"feels like" } - } - div { +"Chance of Rain" } - div { +"Amount" } - div { +"Wind from" } - div { +"Speed" } - if (showGustSpeed) { - div { +"Gusts" } - } - if (showHumidity) { - div { +"Humidity" } - } - div { +"Description" } - div { +"Image" } - } - state.forEach { - div("hour-state") { - div("time") { +"%tH:%(successState)) } @Test fun `fallback query calls all three queries`() { - val successState: AbstractState = object : AbstractState() {} setupQueries(thirdState = successState) query.state() verify(firstQuery).state() @@ -54,14 +54,12 @@ class FallbackQueryTest { @Test fun `fallback query returns second state`() { - val successState: AbstractState = object : AbstractState() {} setupQueries(secondState = successState) assertThat(query.state(), sameInstance(successState)) } @Test fun `fallback query does not query third query`() { - val successState: AbstractState = object : AbstractState() {} setupQueries(secondState = successState) query.state() verify(firstQuery).state() @@ -71,14 +69,12 @@ class FallbackQueryTest { @Test fun `fallback query returns first state`() { - val successState: AbstractState = object : AbstractState() {} setupQueries(firstState = successState) assertThat(query.state(), sameInstance(successState)) } @Test fun `fallback query does not query second and third query`() { - val successState: AbstractState = object : AbstractState() {} setupQueries(firstState = successState) query.state() verify(firstQuery).state() @@ -99,3 +95,5 @@ class FallbackQueryTest { } } + +private val successState: AbstractState = TestState() diff --git a/src/test/kotlin/net/pterodactylus/rhynodge/triggers/AlwaysTriggerTest.kt b/src/test/kotlin/net/pterodactylus/rhynodge/triggers/AlwaysTriggerTest.kt index 0a85bea..6945a0e 100644 --- a/src/test/kotlin/net/pterodactylus/rhynodge/triggers/AlwaysTriggerTest.kt +++ b/src/test/kotlin/net/pterodactylus/rhynodge/triggers/AlwaysTriggerTest.kt @@ -1,11 +1,8 @@ package net.pterodactylus.rhynodge.triggers -import net.pterodactylus.rhynodge.Reaction import net.pterodactylus.rhynodge.State import net.pterodactylus.rhynodge.states.FailedState import net.pterodactylus.rhynodge.states.StateManagerTest.TestState -import net.pterodactylus.rhynodge.testAction -import net.pterodactylus.rhynodge.testQuery import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.sameInstance @@ -30,21 +27,6 @@ class AlwaysTriggerTest { assertThat(trigger.triggers(), equalTo(true)) } - @Test - @Suppress("NonAsciiCharacters") - fun `output returns “true” for plain text`() { - trigger.mergeStates(previousState, successfulState) - val output = trigger.output(Reaction("Test", testQuery(), trigger, testAction())) - assertThat(output.text("text/plain"), equalTo("true")) - } - - @Test - fun `output returns true in a div for html`() { - trigger.mergeStates(previousState, successfulState) - val output = trigger.output(Reaction("Test", testQuery(), trigger, testAction())) - assertThat(output.text("text/html"), equalTo("
true
")) - } - private val trigger = AlwaysTrigger() private val previousState = TestState() private val successfulState: State = TestState() diff --git a/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherTriggerTest.kt b/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherTriggerTest.kt index ca140c0..c4063cb 100644 --- a/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherTriggerTest.kt +++ b/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/WeatherTriggerTest.kt @@ -1,14 +1,9 @@ package net.pterodactylus.rhynodge.webpages.weather -import net.pterodactylus.rhynodge.Reaction import net.pterodactylus.rhynodge.State import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` -import org.hamcrest.Matchers.containsString -import org.hamcrest.Matchers.not import org.junit.Test -import org.mockito.Mockito.mock -import java.io.File import java.time.ZoneId.of import java.time.ZonedDateTime @@ -43,91 +38,4 @@ class WeatherTriggerTest { assertThat(trigger.triggers(), `is`(true)) } - @Test - fun outputContainsCorrectSummary() { - val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin"))) - trigger.mergeStates(previousState, currentState) - val reaction = mock(Reaction::class.java) - val output = trigger.output(reaction) - assertThat(output.summary(), `is`("The Weather (according to Weather) on May 28, 2016")) - } - - @Test - fun outputContainsCorrectHourData() { - val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin"))) - currentState += HourState(0, 10, 11, 0.12, 13.0, WindDirection.SOUTHSOUTHEAST, 14, 15, 0.16, "17", "http://18") - currentState += HourState(1, 20, 21, 0.22, 23.0, WindDirection.NORTHNORTHWEST, 24, 25, 0.26, "27", "http://28") - trigger.mergeStates(previousState, currentState) - val reaction = mock(Reaction::class.java) - val output = trigger.output(reaction) - val htmlText = output.text("text/html") - File("/tmp/wetter.html").writer().use { it.write(htmlText) } - assertThat(htmlText, containsString("00:00")) - assertThat(htmlText, containsString("10 °C")) - assertThat(htmlText, containsString("(11 °C)")) - assertThat(htmlText, containsString("12%")) - assertThat(htmlText, containsString("13 l/m²")) - assertThat(htmlText, containsString("↖↑")) - assertThat(htmlText, containsString("14 km/h")) - assertThat(htmlText, containsString("15 km/h")) - assertThat(htmlText, containsString("16%")) - assertThat(htmlText, containsString("17")) - assertThat(htmlText, containsString("http://18")) - assertThat(htmlText, containsString("01:00")) - assertThat(htmlText, containsString("20 °C")) - assertThat(htmlText, containsString("(21 °C)")) - assertThat(htmlText, containsString("22%")) - assertThat(htmlText, containsString("23 l/m²")) - assertThat(htmlText, containsString("↘↓")) - assertThat(htmlText, containsString("24 km/h")) - assertThat(htmlText, containsString("25 km/h")) - assertThat(htmlText, containsString("26%")) - assertThat(htmlText, containsString("27")) - assertThat(htmlText, containsString("http://28")) - } - - @Test - fun outputContainsColumnHeadersForAllColumns() { - val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin"))) - currentState += HourState(0, 10, 11, 0.12, 13.0, WindDirection.SOUTHSOUTHEAST, 14, 15, 0.16, "17", "http://18") - currentState += HourState(1, 20, 21, 0.22, 23.0, WindDirection.NORTHNORTHWEST, 24, 25, 0.26, "27", "http://28") - trigger.mergeStates(previousState, currentState) - val reaction = mock(Reaction::class.java) - val output = trigger.output(reaction) - val htmlText = output.text("text/html") - assertThat(htmlText, containsString("Time")) - assertThat(htmlText, containsString("Temperature")) - assertThat(htmlText, containsString("feels like")) - assertThat(htmlText, containsString("Chance of Rain")) - assertThat(htmlText, containsString("Amount")) - assertThat(htmlText, containsString("Wind from")) - assertThat(htmlText, containsString("Speed")) - assertThat(htmlText, containsString("Gusts")) - assertThat(htmlText, containsString("Humidity")) - assertThat(htmlText, containsString("Description")) - assertThat(htmlText, containsString("Image")) - } - - @Test - fun outputDoesNotContainColumnHeadersForMissingColumns() { - val currentState = WeatherState("Weather", ZonedDateTime.of(2016, 5, 28, 0, 0, 0, 0, of("Europe/Berlin"))) - currentState += HourState(0, 10, null, 0.12, 13.0, WindDirection.SOUTHSOUTHEAST, 14, null, null, "17", "http://18") - currentState += HourState(1, 20, null, 0.22, 23.0, WindDirection.NORTHNORTHWEST, 24, null, null, "27", "http://28") - trigger.mergeStates(previousState, currentState) - val reaction = mock(Reaction::class.java) - val output = trigger.output(reaction) - val htmlText = output.text("text/html") - assertThat(htmlText, containsString("Time")) - assertThat(htmlText, containsString("Temperature")) - assertThat(htmlText, not(containsString("feels like"))) - assertThat(htmlText, containsString("Chance of Rain")) - assertThat(htmlText, containsString("Amount")) - assertThat(htmlText, containsString("Wind from")) - assertThat(htmlText, containsString("Speed")) - assertThat(htmlText, not(containsString("Gusts"))) - assertThat(htmlText, not(containsString("Humidity"))) - assertThat(htmlText, containsString("Description")) - assertThat(htmlText, containsString("Image")) - } - } diff --git a/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/wetterde/WetterDeFilterTest.kt b/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/wetterde/WetterDeFilterTest.kt index f251cf8..a2430f1 100644 --- a/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/wetterde/WetterDeFilterTest.kt +++ b/src/test/kotlin/net/pterodactylus/rhynodge/webpages/weather/wetterde/WetterDeFilterTest.kt @@ -1,9 +1,9 @@ package net.pterodactylus.rhynodge.webpages.weather.wetterde import net.pterodactylus.rhynodge.filters.ResourceLoader -import net.pterodactylus.rhynodge.states.AbstractState import net.pterodactylus.rhynodge.states.FailedState import net.pterodactylus.rhynodge.states.HtmlState +import net.pterodactylus.rhynodge.states.StateManagerTest.TestState import net.pterodactylus.rhynodge.webpages.weather.HourState import net.pterodactylus.rhynodge.webpages.weather.WeatherState import net.pterodactylus.rhynodge.webpages.weather.WindDirection @@ -36,7 +36,7 @@ class WetterDeFilterTest { @Test fun filterThrowsExceptionIfNotGivenAnHtmlState() { - val successfulHonHtmlState = object : AbstractState(true) {} + val successfulHonHtmlState = TestState() expectedException.expect(IllegalArgumentException::class.java) filter.filter(successfulHonHtmlState) } -- 2.7.4