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"
package net.pterodactylus.rhynodge;
+import javax.annotation.Nonnull;
+
+import net.pterodactylus.rhynodge.output.Output;
+
/**
* Defines the current state of a system.
*
*/
Throwable exception();
+ @Nonnull
+ Output output(Reaction reaction);
+
}
package net.pterodactylus.rhynodge;
-import net.pterodactylus.rhynodge.output.Output;
import net.pterodactylus.rhynodge.states.FileState;
/**
*/
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);
-
}
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()));
}
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.Collections;
import java.util.List;
import net.pterodactylus.rhynodge.Filter;
return new FailedState();
}
- ComicState comicState = new ComicState();
Comic comic = new Comic(title.or(""));
int imageCounter = 0;
for (String imageUrl : imageUrls) {
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));
}
//
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;
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
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 "<div>" + htmlEscaper.escape(plainText()) + "</div>";
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ private static final Escaper htmlEscaper = HtmlEscapers.htmlEscaper();
+
}
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
@JsonProperty
private final List<Comic> comics = Lists.newArrayList();
+ private final Set<Comic> newComics = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ // used for deserialization
+ private ComicState() {
+ }
+
+ public ComicState(Collection<Comic> allComics) {
+ this.comics.addAll(allComics);
+ }
+
+ public ComicState(Collection<Comic> allComics, Collection<Comic> newComics) {
+ this(allComics);
+ this.newComics.addAll(newComics);
+ }
@Override
public boolean isEmpty() {
return comics;
}
- public ComicState add(Comic comic) {
- comics.add(comic);
- return this;
- }
-
@Override
public Iterator<Comic> iterator() {
return comics.iterator();
@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("<body>");
+
+ for (Comic newComic : newComics) {
+ generateComicHtml(html, newComic);
+ }
+
+ List<Comic> 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("</body>").toString();
+ }
+
+ private void generateComicHtml(StringBuilder html, Comic comic) {
+ html.append("<h1>").append(StringEscapeUtils.escapeHtml4(comic.title())).append("</h1>\n");
+ for (Strip strip : comic) {
+ html.append("<div><img src=\"").append(StringEscapeUtils.escapeHtml4(strip.imageUrl()));
+ html.append("\" alt=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
+ html.append("\" title=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
+ html.append("\"></div>\n");
+ html.append("<div>").append(StringEscapeUtils.escapeHtml4(strip.comment())).append("</div>\n");
+ }
}
/**
@Override
public String toString() {
- return String.format("Comic[title=%s,strips=%s]", title(), strips());
+ return format("Comic[title=%s,strips=%s]", title(), strips());
}
}
@Override
public String toString() {
- return String.format("Strip[imageUrl=%s,comment=%s]", imageUrl(), comment());
+ return format("Strip[imageUrl=%s,comment=%s]", imageUrl(), comment());
}
}
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;
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
*/
public class EpisodeState extends AbstractState implements Iterable<Episode> {
- /** The episodes found in the current request. */
+ /**
+ * The episodes found in the current request.
+ */
@JsonProperty
- private final List<Episode> episodes = new ArrayList<Episode>();
+ private final List<Episode> episodes = new ArrayList<>();
+ private final Set<Episode> newEpisodes = new HashSet<>();
+ private final Set<Episode> changedEpisodes = new HashSet<>();
+ private final Set<TorrentFile> newTorrentFiles = new HashSet<>();
/**
* No-arg constructor for deserialization.
*/
@SuppressWarnings("unused")
private EpisodeState() {
- this(Collections.<Episode> emptySet());
}
/**
* Creates a new episode state.
*
- * @param episodes
- * The episodes of the request
+ * @param episodes The episodes of the request
*/
public EpisodeState(Collection<Episode> episodes) {
this.episodes.addAll(episodes);
}
+ public EpisodeState(Collection<Episode> episodes, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes, Collection<TorrentFile> newTorreFiles) {
+ this(episodes);
+ this.newEpisodes.addAll(newEpisodes);
+ this.changedEpisodes.addAll(changedEpisodes);
+ this.newTorrentFiles.addAll(newTorreFiles);
+ }
+
//
// ACCESSORS
//
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<Integer, Collection<Episode>> episodesBySeason = FluentIterable.from(episodes).index(Episode::season).asMap();
+ for (Map.Entry<Integer, Collection<Episode>> 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("<html><body>\n");
+ /* show all known episodes. */
+ htmlBuilder.append("<table>\n<caption>All Known Episodes</caption>\n");
+ htmlBuilder.append("<thead>\n");
+ htmlBuilder.append("<tr>");
+ htmlBuilder.append("<th>Season</th>");
+ htmlBuilder.append("<th>Episode</th>");
+ htmlBuilder.append("<th>Filename</th>");
+ htmlBuilder.append("<th>Size</th>");
+ htmlBuilder.append("<th>File(s)</th>");
+ htmlBuilder.append("<th>Seeds</th>");
+ htmlBuilder.append("<th>Leechers</th>");
+ htmlBuilder.append("<th>Magnet</th>");
+ htmlBuilder.append("<th>Download</th>");
+ htmlBuilder.append("</tr>\n");
+ htmlBuilder.append("</thead>\n");
+ htmlBuilder.append("<tbody>\n");
+ Episode lastEpisode = null;
+ for (Map.Entry<Integer, Collection<Episode>> 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("<tr style=\"color: #008000; font-weight: bold;\">");
+ } else if (newTorrentFiles.contains(torrentFile)) {
+ htmlBuilder.append("<tr style=\"color: #008000;\">");
+ } else {
+ htmlBuilder.append("<tr>");
+ }
+ if ((lastEpisode == null) || !lastEpisode.equals(episode)) {
+ htmlBuilder.append("<td>").append(episode.season()).append("</td><td>").append(episode.episode()).append("</td>");
+ } else {
+ htmlBuilder.append("<td colspan=\"2\"></td>");
+ }
+ htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
+ htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
+ htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
+ htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
+ htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
+ htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
+ htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
+ htmlBuilder.append("</tr>\n");
+ lastEpisode = episode;
+ }
+ }
+ }
+ htmlBuilder.append("</tbody>\n");
+ htmlBuilder.append("</table>\n");
+ htmlBuilder.append("</body></html>\n");
+ return htmlBuilder.toString();
+ }
+
//
// ITERABLE INTERFACE
//
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;
/**
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
//
package net.pterodactylus.rhynodge.states;
+import javax.annotation.Nonnull;
+
import net.pterodactylus.rhynodge.State;
/**
return modificationTime;
}
+ @Nonnull
+ @Override
+ protected String plainText() {
+ return toString();
+ }
+
//
// OBJECT METHODS
//
package net.pterodactylus.rhynodge.states;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
import net.pterodactylus.rhynodge.State;
import org.jsoup.nodes.Document;
return document;
}
+ @Nonnull
+ @Override
+ protected String plainText() {
+ //noinspection ConstantConditions
+ return htmlText();
+ }
+
+ @Nullable
+ @Override
+ protected String htmlText() {
+ return document.toString();
+ }
+
//
// OBJECT METHODS
//
import java.io.UnsupportedEncodingException;
+import javax.annotation.Nonnull;
+
import net.pterodactylus.rhynodge.State;
import net.pterodactylus.rhynodge.queries.HttpQuery;
}
}
+ @Nonnull
+ @Override
+ protected String plainText() {
+ return content();
+ }
+
//
// STATIC METHODS
//
import java.util.Optional;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
import net.pterodactylus.rhynodge.State;
import com.fasterxml.jackson.annotation.JsonProperty;
return !plainTextOutput.isPresent() && !htmlOutput.isPresent();
}
- public Optional<String> plainTextOutput() {
- return plainTextOutput;
+ @Nonnull
+ @Override
+ protected String plainText() {
+ return plainTextOutput.orElse("");
}
- public Optional<String> htmlOutput() {
- return htmlOutput;
+ @Nullable
+ @Override
+ protected String htmlText() {
+ return htmlOutput.orElse(null);
}
}
package net.pterodactylus.rhynodge.states;
+import javax.annotation.Nonnull;
+
/**
* A {@link net.pterodactylus.rhynodge.State} that stores a single {@link
* String} value.
return value.isEmpty();
}
+ @Nonnull
+ @Override
+ protected String plainText() {
+ return value;
+ }
+
}
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
@JsonProperty
private List<TorrentFile> files = Lists.newArrayList();
+ private final Set<TorrentFile> newTorrentFiles = new HashSet<>();
+
/**
* Creates a new torrent state without torrent files.
*/
public TorrentState() {
- this(Collections.<TorrentFile> emptySet());
+ this(Collections.<TorrentFile>emptySet());
}
/**
files.addAll(torrentFiles);
}
+ public TorrentState(Collection<TorrentFile> torrentFiles, Collection<TorrentFile> newTorrentFiles) {
+ files.addAll(torrentFiles);
+ this.newTorrentFiles.addAll(newTorrentFiles);
+ }
+
//
// ACCESSORS
//
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("<html><body>\n");
+ htmlBuilder.append("<table>\n<caption>All Known Torrents</caption>\n");
+ htmlBuilder.append("<thead>\n");
+ htmlBuilder.append("<tr>");
+ htmlBuilder.append("<th>Filename</th>");
+ htmlBuilder.append("<th>Size</th>");
+ htmlBuilder.append("<th>File(s)</th>");
+ htmlBuilder.append("<th>Seeds</th>");
+ htmlBuilder.append("<th>Leechers</th>");
+ htmlBuilder.append("<th>Magnet</th>");
+ htmlBuilder.append("<th>Download</th>");
+ htmlBuilder.append("</tr>\n");
+ htmlBuilder.append("</thead>\n");
+ htmlBuilder.append("<tbody>\n");
+ for (TorrentFile torrentFile : sortNewFirst().sortedCopy(files)) {
+ if (newTorrentFiles.contains(torrentFile)) {
+ htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
+ } else {
+ htmlBuilder.append("<tr>");
+ }
+ htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
+ htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
+ htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
+ htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
+ htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
+ htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
+ htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
+ htmlBuilder.append("</tr>\n");
+ }
+ htmlBuilder.append("</tbody>\n");
+ htmlBuilder.append("</table>\n");
+ htmlBuilder.append("</body></html>\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<TorrentFile> 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
//
*/
@Override
public String toString() {
- return String.format("%s[files=%s]", getClass().getSimpleName(), files);
+ return format("%s[files=%s]", getClass().getSimpleName(), files);
}
/**
*/
@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());
}
}
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.
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", "<div>true</div>");
- }
-
}
return triggered;
}
- /**
- * {@inheritDoc}
- */
- @Override
- public Output output(Reaction reaction) {
- return new DefaultOutput("File appeared/disappeared").addText("text/plain", "File appeared/disappeared").addText("text/html", "<div>File appeared/disappeared</div>");
- }
-
}
return triggered;
}
- /**
- * {@inheritDoc}
- */
- @Override
- public Output output(Reaction reaction) {
- return new DefaultOutput("File modified").addText("text/plain", "File modified").addText("text/html", "<div>File modified</div>");
- }
-
}
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
*/
public class NewComicTrigger implements Trigger {
- /** The new comics. */
- private final List<Comic> newComics = Lists.newArrayList();
-
- /** The latest comic state. */
- private ComicState mergedComicState;
+ private boolean triggered = false;
@Override
public State mergeStates(State previousState, State currentState) {
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<Comic> allComics = new HashSet<>(previousComicState.comics());
+ Set<Comic> 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("<body>");
-
- for (Comic newComic : newComics) {
- generateComicHtml(html, newComic);
- }
-
- List<Comic> latestComics = new ArrayList<Comic>(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("</body>").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("<h1>").append(StringEscapeUtils.escapeHtml4(comic.title())).append("</h1>\n");
- for (Strip strip : comic) {
- html.append("<div><img src=\"").append(StringEscapeUtils.escapeHtml4(strip.imageUrl()));
- html.append("\" alt=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
- html.append("\" title=\"").append(StringEscapeUtils.escapeHtml4(strip.comment()));
- html.append("\"></div>\n");
- html.append("<div>").append(StringEscapeUtils.escapeHtml4(strip.comment())).append("</div>\n");
- }
+ return triggered;
}
}
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
*/
public class NewEpisodeTrigger implements Trigger {
- /** All episodes. */
- private final Collection<Episode> allEpisodes = Sets.newHashSet();
-
- /** All new episodes. */
- private final Collection<Episode> newEpisodes = Sets.newHashSet();
-
- /** All changed episodes. */
- private final Collection<Episode> changedEpisodes = Sets.newHashSet();
-
- /** All new torrent files. */
- private final Collection<TorrentFile> newTorrentFiles = Sets.newHashSet();
-
- //
- // TRIGGER METHODS
- //
+ private boolean triggered = false;
/**
* {@inheritDoc}
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<Episode, Episode> allEpisodes = Maps.newHashMap(FluentIterable.from(((EpisodeState) previousState).episodes()).toMap(new Function<Episode, Episode>() {
- @Override
- public Episode apply(Episode episode) {
- return episode;
- }
- }));
+ Collection<Episode> newEpisodes = new HashSet<>();
+ Collection<Episode> changedEpisodes = new HashSet<>();
+ Collection<TorrentFile> newTorrentFiles = new HashSet<>();
+ Map<Episode, Episode> 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();
}
}
}
- this.allEpisodes.addAll(allEpisodes.values());
- return new EpisodeState(this.allEpisodes);
+ return new EpisodeState(allEpisodes.values(), newEpisodes, changedEpisodes, newTorrentFiles);
}
/**
*/
@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<Integer, Collection<Episode>> episodesBySeason = FluentIterable.from(allEpisodes).index(new Function<Episode, Integer>() {
-
- @Override
- public Integer apply(Episode episode) {
- return episode.season();
- }
- }).asMap();
- for (Entry<Integer, Collection<Episode>> 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("<html><body>\n");
- /* show all known episodes. */
- htmlBuilder.append("<table>\n<caption>All Known Episodes</caption>\n");
- htmlBuilder.append("<thead>\n");
- htmlBuilder.append("<tr>");
- htmlBuilder.append("<th>Season</th>");
- htmlBuilder.append("<th>Episode</th>");
- htmlBuilder.append("<th>Filename</th>");
- htmlBuilder.append("<th>Size</th>");
- htmlBuilder.append("<th>File(s)</th>");
- htmlBuilder.append("<th>Seeds</th>");
- htmlBuilder.append("<th>Leechers</th>");
- htmlBuilder.append("<th>Magnet</th>");
- htmlBuilder.append("<th>Download</th>");
- htmlBuilder.append("</tr>\n");
- htmlBuilder.append("</thead>\n");
- htmlBuilder.append("<tbody>\n");
- Episode lastEpisode = null;
- for (Entry<Integer, Collection<Episode>> 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("<tr style=\"color: #008000; font-weight: bold;\">");
- } else if (newTorrentFiles.contains(torrentFile)) {
- htmlBuilder.append("<tr style=\"color: #008000;\">");
- } else {
- htmlBuilder.append("<tr>");
- }
- if ((lastEpisode == null) || !lastEpisode.equals(episode)) {
- htmlBuilder.append("<td>").append(episode.season()).append("</td><td>").append(episode.episode()).append("</td>");
- } else {
- htmlBuilder.append("<td colspan=\"2\"></td>");
- }
- htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
- htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
- htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
- htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
- htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
- htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
- htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
- htmlBuilder.append("</tr>\n");
- lastEpisode = episode;
- }
- }
- }
- htmlBuilder.append("</tbody>\n");
- htmlBuilder.append("</table>\n");
- htmlBuilder.append("</body></html>\n");
- return htmlBuilder.toString();
+ return triggered;
}
}
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
*/
public class NewTorrentTrigger implements Trigger {
- /** All known torrents. */
- private final Set<TorrentFile> allTorrentFiles = Sets.newHashSet();
-
- /** The newly detected torrent files. */
- private final List<TorrentFile> newTorrentFiles = Lists.newArrayList();
+ private boolean triggered = false;
//
// TRIGGER METHODS
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<TorrentFile> allTorrentFiles = new HashSet<>(((TorrentState) previousState).torrentFiles());
+ Set<TorrentFile> 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);
}
/**
*/
@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("<html><body>\n");
- htmlBuilder.append("<table>\n<caption>All Known Torrents</caption>\n");
- htmlBuilder.append("<thead>\n");
- htmlBuilder.append("<tr>");
- htmlBuilder.append("<th>Filename</th>");
- htmlBuilder.append("<th>Size</th>");
- htmlBuilder.append("<th>File(s)</th>");
- htmlBuilder.append("<th>Seeds</th>");
- htmlBuilder.append("<th>Leechers</th>");
- htmlBuilder.append("<th>Magnet</th>");
- htmlBuilder.append("<th>Download</th>");
- htmlBuilder.append("</tr>\n");
- htmlBuilder.append("</thead>\n");
- htmlBuilder.append("<tbody>\n");
- for (TorrentFile torrentFile : sortNewFirst().sortedCopy(allTorrentFiles)) {
- if (newTorrentFiles.contains(torrentFile)) {
- htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
- } else {
- htmlBuilder.append("<tr>");
- }
- htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
- htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
- htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
- htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
- htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
- htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
- htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
- htmlBuilder.append("</tr>\n");
- }
- htmlBuilder.append("</tbody>\n");
- htmlBuilder.append("</table>\n");
- htmlBuilder.append("</body></html>\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<TorrentFile> sortNewFirst() {
- return new Ordering<TorrentFile>() {
-
- @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;
}
}
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.
*/
class WeatherState(val service: String, val dateTime: ZonedDateTime) : AbstractState(true), Iterable<HourState> {
- 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<HourState> = mutableListOf()
+ @JsonProperty("hours")
+ val hours: List<HourState> = 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<HourState>).add(hourState)
- }
+ operator fun plusAssign(hourState: HourState) {
+ (hours as MutableList<HourState>).add(hourState)
+ }
- override fun iterator(): Iterator<HourState> {
- return hours.iterator()
- }
+ override fun iterator(): Iterator<HourState> {
+ 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:%<tM".format(startTime.plus(it.hourIndex.toLong(), ChronoUnit.HOURS).toEpochMilli()) }
+ div("temperature") { +"${it.temperature} °C" }
+ if (showFeltTemperature) {
+ div("felt-temperature") { +if (it.feltTemperature != null) "(${it.feltTemperature} °C)" else "" }
+ }
+ div("rain-probability") { +"${it.rainProbability.times(100).toInt()}%" }
+ div("rain-amount") { +"${it.rainAmount.minDigits()} l/m²" }
+ div("wind-direction") { +it.windDirection.arrow }
+ div("wind-speed") { +"${it.windSpeed} km/h" }
+ if (showGustSpeed) {
+ div("gust-speed") { +if (it.gustSpeed != null) "(up to ${it.gustSpeed} km/h)" else "" }
+ }
+ if (showHumidity) {
+ div("humidity") { +if (it.humidity != null) "${it.humidity.times(100).toInt()}%" else "" }
+ }
+ div("description") { +it.description }
+ div("image") { img(src = it.image) }
+ }
+ }
+ }
+ }
+ }.toString()
}
+
+private val dateFormatter: DateFormat =
+ DateFormat.getDateInstance(DateFormat.LONG, Locale.ENGLISH)
+
+private fun Double.minDigits(): String =
+ toString().replace(Regex("\\.0*$"), "")
package net.pterodactylus.rhynodge.webpages.weather
-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.Reaction
import net.pterodactylus.rhynodge.State
import net.pterodactylus.rhynodge.Trigger
-import net.pterodactylus.rhynodge.output.DefaultOutput
-import net.pterodactylus.rhynodge.output.Output
-import java.text.DateFormat
-import java.time.temporal.ChronoUnit
-import java.util.Locale
/**
* Detects changes in the weather and creates email texts.
*/
class WeatherTrigger : Trigger {
- private val dateFormatter = DateFormat.getDateInstance(DateFormat.LONG, Locale.ENGLISH)
- private lateinit var state: WeatherState
- private var changed = false
+ private lateinit var state: WeatherState
+ private var changed = false
- override fun mergeStates(previousState: State, currentState: State): State {
- changed = previousState != currentState
- state = currentState as WeatherState
- return currentState
- }
+ override fun mergeStates(previousState: State, currentState: State): State {
+ changed = previousState != currentState
+ state = currentState as WeatherState
+ return currentState
+ }
- override fun triggers(): Boolean {
- return changed
- }
-
- override fun output(reaction: Reaction): Output {
- val output = DefaultOutput("The Weather (according to ${state.service}) on ${dateFormatter.format(state.dateTime.toInstant().toEpochMilli())}")
- output.addText("text/html", generateHtmlOutput())
- return output
- }
-
- private fun generateHtmlOutput(): String {
- return 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 = 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:%<tM".format(startTime.plus(it.hourIndex.toLong(), ChronoUnit.HOURS).toEpochMilli()) }
- div("temperature") { +"${it.temperature} °C" }
- if (showFeltTemperature) {
- div("felt-temperature") { +if (it.feltTemperature != null) "(${it.feltTemperature} °C)" else "" }
- }
- div("rain-probability") { +"${it.rainProbability.times(100).toInt()}%" }
- div("rain-amount") { +"${it.rainAmount.minDigits()} l/m²" }
- div("wind-direction") { +it.windDirection.arrow }
- div("wind-speed") { +"${it.windSpeed} km/h" }
- if (showGustSpeed) {
- div("gust-speed") { +if (it.gustSpeed != null) "(up to ${it.gustSpeed} km/h)" else "" }
- }
- if (showHumidity) {
- div("humidity") { +if (it.humidity != null) "${it.humidity.times(100).toInt()}%" else "" }
- }
- div("description") { +it.description }
- div("image") { img(src = it.image) }
- }
- }
- }
- }
- }.toString()
- }
-
- private fun Double.minDigits(): String {
- return this.toString().replace(Regex("\\.0*$"), "")
- }
+ override fun triggers() = changed
}
package net.pterodactylus.rhynodge.states;
-import static com.google.common.base.Charsets.UTF_8;
-import static com.google.common.base.Objects.equal;
-import static org.apache.log4j.Level.OFF;
-import static org.apache.log4j.Logger.getLogger;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
import java.io.File;
import java.io.IOException;
import java.util.Optional;
+import javax.annotation.Nonnull;
import net.pterodactylus.rhynodge.State;
import net.pterodactylus.rhynodge.states.StateManager.StateDirectory;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
+import static com.google.common.base.Charsets.UTF_8;
+import static com.google.common.base.Objects.equal;
+import static org.apache.log4j.Level.OFF;
+import static org.apache.log4j.Logger.getLogger;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
/**
* Unit test for {@link StateManager}.
*
&& (time() == testState.time());
}
+ @Nonnull
+ @Override
+ protected String plainText() {
+ return "Test";
+ }
+
}
public static class InvalidState extends AbstractState {
@JsonProperty
private final Object someObject = new Object();
+ @Nonnull
+ @Override
+ protected String plainText() {
+ return "Invalid";
+ }
+
}
}
import net.pterodactylus.rhynodge.State
import net.pterodactylus.rhynodge.states.AbstractState
import net.pterodactylus.rhynodge.states.FailedState
+import net.pterodactylus.rhynodge.states.StateManagerTest
+import net.pterodactylus.rhynodge.states.StateManagerTest.TestState
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.sameInstance
@Test
fun `fallback query returns state of third query`() {
- val successState: AbstractState = object : AbstractState() {}
setupQueries(thirdState = successState)
assertThat(query.state(), sameInstance<State>(successState))
}
@Test
fun `fallback query calls all three queries`() {
- val successState: AbstractState = object : AbstractState() {}
setupQueries(thirdState = successState)
query.state()
verify(firstQuery).state()
@Test
fun `fallback query returns second state`() {
- val successState: AbstractState = object : AbstractState() {}
setupQueries(secondState = successState)
assertThat(query.state(), sameInstance<State>(successState))
}
@Test
fun `fallback query does not query third query`() {
- val successState: AbstractState = object : AbstractState() {}
setupQueries(secondState = successState)
query.state()
verify(firstQuery).state()
@Test
fun `fallback query returns first state`() {
- val successState: AbstractState = object : AbstractState() {}
setupQueries(firstState = successState)
assertThat(query.state(), sameInstance<State>(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()
}
}
+
+private val successState: AbstractState = TestState()
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
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("<div>true</div>"))
- }
-
private val trigger = AlwaysTrigger()
private val previousState = TestState()
private val successfulState: State = TestState()
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
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"))
- }
-
}
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
@Test
fun filterThrowsExceptionIfNotGivenAnHtmlState() {
- val successfulHonHtmlState = object : AbstractState(true) {}
+ val successfulHonHtmlState = TestState()
expectedException.expect(IllegalArgumentException::class.java)
filter.filter(successfulHonHtmlState)
}