--- /dev/null
- import static com.google.common.base.Preconditions.checkState;
-
- import java.net.URI;
- import java.net.URISyntaxException;
-
+/*
+ * Rhynodge - KickAssTorrentsFilter.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.filters;
+
- import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.Filter;
- import net.pterodactylus.rhynodge.states.FailedState;
+import net.pterodactylus.rhynodge.queries.HttpQuery;
- import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+import net.pterodactylus.rhynodge.states.HtmlState;
+import net.pterodactylus.rhynodge.states.TorrentState;
- public class KickAssTorrentsFilter implements Filter {
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+/**
+ * {@link Filter} implementation that parses a {@link TorrentState} from an
+ * {@link HtmlState} which was generated by a {@link HttpQuery} to
+ * {@code kickasstorrents.ph}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
- public State filter(State state) {
- if (!state.success()) {
- return FailedState.from(state);
- }
- checkState(state instanceof HtmlState, "state is not an HtmlState but a %s", state.getClass().getName());
-
- /* get result table. */
- Document document = ((HtmlState) state).document();
- Elements mainTable = document.select("table.data");
- if (mainTable.isEmpty()) {
- /* no main table? */
- return new FailedState();
- }
-
- /* iterate over all rows. */
- TorrentState torrentState = new TorrentState();
- Elements dataRows = mainTable.select("tr:gt(0)");
- for (Element dataRow : dataRows) {
- String name = extractName(dataRow);
- String size = extractSize(dataRow);
- String magnetUri = extractMagnetUri(dataRow);
- String downloadUri;
- int fileCount = extractFileCount(dataRow);
- int seedCount = extractSeedCount(dataRow);
- int leechCount = extractLeechCount(dataRow);
- try {
- downloadUri = new URI(((HtmlState) state).uri()).resolve(extractDownloadUri(dataRow)).toString();
- TorrentFile torrentFile = new TorrentFile(name, size, magnetUri, downloadUri, fileCount, seedCount, leechCount);
- torrentState.addTorrentFile(torrentFile);
- } catch (URISyntaxException use1) {
- /* ignore; if uri was wrong, we wouldn’t be here. */
- }
- }
-
- return torrentState;
++public class KickAssTorrentsFilter extends TorrentSiteFilter {
++
++ //
++ // TORRENTSITEFILTER METHODS
++ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
- //
- // STATIC METHODS
- //
-
++ protected Elements getDataRows(Document document) {
++ return document.select("table.data").select("tr:gt(0)");
+ }
+
- * Extracts the name from the given row.
- *
- * @param dataRow
- * The row to extract the name from
- * @return The extracted name
+ /**
- private static String extractName(Element dataRow) {
++ * {@inheritDoc}
+ */
- * Extracts the size from the given row.
- *
- * @param dataRow
- * The row to extract the size from
- * @return The extracted size
++ @Override
++ protected String extractName(Element dataRow) {
+ return dataRow.select("div.torrentname a.normalgrey").text();
+ }
+
+ /**
- private static String extractSize(Element dataRow) {
++ * {@inheritDoc}
+ */
- * Extracts the magnet URI from the given row.
- *
- * @param dataRow
- * The row to extract the magnet URI from
- * @return The extracted magnet URI
++ @Override
++ protected String extractSize(Element dataRow) {
+ return dataRow.select("td:eq(1)").text();
+ }
+
+ /**
- private static String extractMagnetUri(Element dataRow) {
++ * {@inheritDoc}
+ */
- * Extracts the download URI from the given row.
- *
- * @param dataRow
- * The row to extract the download URI from
- * @return The extracted download URI
++ @Override
++ protected String extractMagnetUri(Element dataRow) {
+ return dataRow.select("a.imagnet").attr("href");
+ }
+
+ /**
- private static String extractDownloadUri(Element dataRow) {
++ * {@inheritDoc}
+ */
- * Extracts the file count from the given row.
- *
- * @param dataRow
- * The row to extract the file count from
- * @return The extracted file count
++ @Override
++ protected String extractDownloadUri(Element dataRow) {
+ return dataRow.select("a.idownload:not(.partner1Button)").attr("href");
+ }
+
+ /**
- private static int extractFileCount(Element dataRow) {
++ * {@inheritDoc}
+ */
- * Extracts the seed count from the given row.
- *
- * @param dataRow
- * The row to extract the seed count from
- * @return The extracted seed count
++ @Override
++ protected int extractFileCount(Element dataRow) {
+ return Integer.valueOf(dataRow.select("td:eq(2)").text());
+ }
+
+ /**
- private static int extractSeedCount(Element dataRow) {
++ * {@inheritDoc}
+ */
- * Extracts the leech count from the given row.
- *
- * @param dataRow
- * The row to extract the leech count from
- * @return The extracted leech count
++ @Override
++ protected int extractSeedCount(Element dataRow) {
+ return Integer.valueOf(dataRow.select("td:eq(4)").text());
+ }
+
+ /**
- private static int extractLeechCount(Element dataRow) {
++ * {@inheritDoc}
+ */
++ @Override
++ protected int extractLeechCount(Element dataRow) {
+ return Integer.valueOf(dataRow.select("td:eq(5)").text());
+ }
+
+}
--- /dev/null
--- /dev/null
++/*
++ * Rhynodge - PirateBayFilter.java - Copyright © 2013 David Roden
++ *
++ * This program is free software: you can redistribute it and/or modify
++ * it under the terms of the GNU General Public License as published by
++ * the Free Software Foundation, either version 3 of the License, or
++ * (at your option) any later version.
++ *
++ * This program is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ * GNU General Public License for more details.
++ *
++ * You should have received a copy of the GNU General Public License
++ * along with this program. If not, see <http://www.gnu.org/licenses/>.
++ */
++
++package net.pterodactylus.rhynodge.filters;
++
++import java.util.regex.Pattern;
++
++import org.jsoup.nodes.Document;
++import org.jsoup.nodes.Element;
++import org.jsoup.select.Elements;
++
++/**
++ * {@link TorrentSiteFilter} implementation that can parse
++ * {@code thepiratebay.se} result pages.
++ *
++ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
++ */
++public class PirateBayFilter extends TorrentSiteFilter {
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected Elements getDataRows(Document document) {
++ return document.select("table#searchResult tbody tr:has(.vertTh)");
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected String extractName(Element dataRow) {
++ return dataRow.select(".detName a").text();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected String extractSize(Element dataRow) {
++ return dataRow.select(".detDesc").text().split(Pattern.quote(","))[1].trim();
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected String extractMagnetUri(Element dataRow) {
++ return dataRow.select("a[href^=magnet:]").attr("href");
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected String extractDownloadUri(Element dataRow) {
++ return dataRow.select("a[href^=//torrents.]").attr("href");
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected int extractFileCount(Element dataRow) {
++ return 0;
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected int extractSeedCount(Element dataRow) {
++ return Integer.valueOf(dataRow.select("td:eq(2)").text());
++ }
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ protected int extractLeechCount(Element dataRow) {
++ return Integer.valueOf(dataRow.select("td:eq(3)").text());
++ }
++
++}
--- /dev/null
--- /dev/null
++/*
++ * Rhynodge - KickAssTorrentsFilter.java - Copyright © 2013 David Roden
++ *
++ * This program is free software: you can redistribute it and/or modify
++ * it under the terms of the GNU General Public License as published by
++ * the Free Software Foundation, either version 3 of the License, or
++ * (at your option) any later version.
++ *
++ * This program is distributed in the hope that it will be useful,
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ * GNU General Public License for more details.
++ *
++ * You should have received a copy of the GNU General Public License
++ * along with this program. If not, see <http://www.gnu.org/licenses/>.
++ */
++
++package net.pterodactylus.rhynodge.filters;
++
++import static com.google.common.base.Preconditions.checkState;
++
++import java.io.UnsupportedEncodingException;
++import java.net.URI;
++import java.net.URISyntaxException;
++import java.net.URLEncoder;
++
++import net.pterodactylus.rhynodge.Filter;
++import net.pterodactylus.rhynodge.State;
++import net.pterodactylus.rhynodge.queries.HttpQuery;
++import net.pterodactylus.rhynodge.states.FailedState;
++import net.pterodactylus.rhynodge.states.HtmlState;
++import net.pterodactylus.rhynodge.states.TorrentState;
++import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
++
++import org.jsoup.nodes.Document;
++import org.jsoup.nodes.Element;
++import org.jsoup.select.Elements;
++
++/**
++ * {@link Filter} implementation that parses a {@link TorrentState} from an
++ * {@link HtmlState} which was generated by a {@link HttpQuery} to
++ * {@code kickasstorrents.ph}.
++ *
++ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
++ */
++public abstract class TorrentSiteFilter implements Filter {
++
++ /**
++ * {@inheritDoc}
++ */
++ @Override
++ public State filter(State state) {
++ if (!state.success()) {
++ return FailedState.from(state);
++ }
++ checkState(state instanceof HtmlState, "state is not an HtmlState but a %s", state.getClass().getName());
++
++ /* get result table. */
++ Document document = ((HtmlState) state).document();
++
++ /* iterate over all rows. */
++ Elements dataRows = getDataRows(document);
++ TorrentState torrentState = new TorrentState();
++ for (Element dataRow : dataRows) {
++ String name = extractName(dataRow);
++ String size = extractSize(dataRow);
++ String magnetUri = extractMagnetUri(dataRow);
++ String downloadUri = extractDownloadUri(dataRow);
++ int fileCount = extractFileCount(dataRow);
++ int seedCount = extractSeedCount(dataRow);
++ int leechCount = extractLeechCount(dataRow);
++ try {
++ if ((downloadUri != null) && (downloadUri.length() > 0)) {
++ downloadUri = new URI(((HtmlState) state).uri()).resolve(URLEncoder.encode(downloadUri, "UTF-8").replace("%2F", "/")).toString();
++ } else {
++ downloadUri = null;
++ }
++ TorrentFile torrentFile = new TorrentFile(name, size, magnetUri, downloadUri, fileCount, seedCount, leechCount);
++ torrentState.addTorrentFile(torrentFile);
++ } catch (URISyntaxException use1) {
++ /* ignore; if uri was wrong, we wouldn’t be here. */
++ } catch (UnsupportedEncodingException uee1) {
++ /* ignore, all JVMs can do UTF-8. */
++ }
++ }
++
++ return torrentState;
++ }
++
++ //
++ // ABSTRACT METHODS
++ //
++
++ /**
++ * Returns the data rows from the given document.
++ *
++ * @param document
++ * The document to get the data rows from
++ * @return The data rows
++ */
++ protected abstract Elements getDataRows(Document document);
++
++ /**
++ * Extracts the name from the given row.
++ *
++ * @param dataRow
++ * The row to extract the name from
++ * @return The extracted name
++ */
++ protected abstract String extractName(Element dataRow);
++
++ /**
++ * Extracts the size from the given row.
++ *
++ * @param dataRow
++ * The row to extract the size from
++ * @return The extracted size
++ */
++ protected abstract String extractSize(Element dataRow);
++
++ /**
++ * Extracts the magnet URI from the given row.
++ *
++ * @param dataRow
++ * The row to extract the magnet URI from
++ * @return The extracted magnet URI
++ */
++ protected abstract String extractMagnetUri(Element dataRow);
++
++ /**
++ * Extracts the download URI from the given row.
++ *
++ * @param dataRow
++ * The row to extract the download URI from
++ * @return The extracted download URI
++ */
++ protected abstract String extractDownloadUri(Element dataRow);
++
++ /**
++ * Extracts the file count from the given row.
++ *
++ * @param dataRow
++ * The row to extract the file count from
++ * @return The extracted file count, or {@code 0} if the file count can not
++ * be extracted
++ */
++ protected abstract int extractFileCount(Element dataRow);
++
++ /**
++ * Extracts the seed count from the given row.
++ *
++ * @param dataRow
++ * The row to extract the seed count from
++ * @return The extracted seed count
++ */
++ protected abstract int extractSeedCount(Element dataRow);
++
++ /**
++ * Extracts the leech count from the given row.
++ *
++ * @param dataRow
++ * The row to extract the leech count from
++ * @return The extracted leech count
++ */
++ protected abstract int extractLeechCount(Element dataRow);
++
++}
--- /dev/null
- * @return The magnet URI of the file
+/*
+ * Rhynodge - TorrentState.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.states;
+
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
+
+import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.Lists;
+
+/**
+ * {@link State} that contains information about an arbitrary number of torrent
+ * files.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TorrentState extends AbstractState implements Iterable<TorrentFile> {
+
+ /** The torrent files. */
+ @JsonProperty
+ private List<TorrentFile> files = Lists.newArrayList();
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Adds a torrent file to this state.
+ *
+ * @param torrentFile
+ * The torrent file to add
+ * @return This state
+ */
+ public TorrentState addTorrentFile(TorrentFile torrentFile) {
+ files.add(torrentFile);
+ return this;
+ }
+
+ //
+ // ITERABLE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator<TorrentFile> iterator() {
+ return files.iterator();
+ }
+
+ //
+ // OBJECT METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return String.format("%s[files=%s]", getClass().getSimpleName(), files);
+ }
+
+ /**
+ * Container for torrent file data.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public static class TorrentFile {
+
+ /** The name of the file. */
+ @JsonProperty
+ private final String name;
+
+ /** The size of the file. */
+ @JsonProperty
+ private final String size;
+
+ /** The magnet URI of the file. */
+ @JsonProperty
+ private final String magnetUri;
+
+ /** The download URI of the file. */
+ @JsonProperty
+ private final String downloadUri;
+
+ /** The number of files in this torrent. */
+ @JsonProperty
+ private final int fileCount;
+
+ /** The number of seeds connected to this torrent. */
+ @JsonProperty
+ private final int seedCount;
+
+ /** The number of leechers connected to this torrent. */
+ @JsonProperty
+ private final int leechCount;
+
+ /**
+ * No-arg constructor for deserialization.
+ */
+ @SuppressWarnings("unused")
+ private TorrentFile() {
+ this(null, null, null, null, 0, 0, 0);
+ }
+
+ /**
+ * Creates a new torrent file.
+ *
+ * @param name
+ * The name of the file
+ * @param size
+ * The size of the file
+ * @param magnetUri
+ * The magnet URI of the file
+ * @param downloadUri
+ * The download URI of the file
+ * @param fileCount
+ * The number of files
+ * @param seedCount
+ * The number of connected seeds
+ * @param leechCount
+ * The number of connected leechers
+ */
+ public TorrentFile(String name, String size, String magnetUri, String downloadUri, int fileCount, int seedCount, int leechCount) {
+ this.name = name;
+ this.size = size;
+ this.magnetUri = magnetUri;
+ this.downloadUri = downloadUri;
+ this.fileCount = fileCount;
+ this.seedCount = seedCount;
+ this.leechCount = leechCount;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns the name of the file.
+ *
+ * @return The name of the file
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns the size of the file. The returned size may included
+ * non-numeric information, such as units (e. g. “860.46 MB”).
+ *
+ * @return The size of the file
+ */
+ public String size() {
+ return size;
+ }
+
+ /**
+ * Returns the magnet URI of the file.
+ *
- * @return The download URI of the file
++ * @return The magnet URI of the file, or {@code null} if there is no
++ * magnet URI for this torrent file
+ */
+ public String magnetUri() {
+ return magnetUri;
+ }
+
+ /**
+ * Returns the download URI of the file.
+ *
++ * @return The download URI of the file, or {@code null} if there is no
++ * download URI for this torrent file
+ */
+ public String downloadUri() {
+ return downloadUri;
+ }
+
+ /**
+ * Returns the number of files in this torrent.
+ *
+ * @return The number of files in this torrent
+ */
+ public int fileCount() {
+ return fileCount;
+ }
+
+ /**
+ * Returns the number of seeds connected to this torrent.
+ *
+ * @return The number of connected seeds
+ */
+ public int seedCount() {
+ return seedCount;
+ }
+
+ /**
+ * Returns the number of leechers connected to this torrent.
+ *
+ * @return The number of connected leechers
+ */
+ public int leechCount() {
+ return leechCount;
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Generates an ID for this file. If a {@link #magnetUri} is set, an ID
+ * is {@link #extractId(String) extracted} from it. Otherwise the magnet
+ * URI is used. If the {@link #magnetUri} is not set, the
+ * {@link #downloadUri} is used. If that is not set either, the name of
+ * the file is returned.
+ *
+ * @return The generated ID
+ */
+ private String generateId() {
+ if (magnetUri != null) {
+ String id = extractId(magnetUri);
+ if (id != null) {
+ return id;
+ }
+ return magnetUri;
+ }
+ return (downloadUri != null) ? downloadUri : name;
+ }
+
+ //
+ // STATIC METHODS
+ //
+
+ /**
+ * Tries to extract the “exact target” of a magnet URI.
+ *
+ * @param magnetUri
+ * The magnet URI to extract the “xt” from
+ * @return The extracted ID, or {@code null} if no ID could be found
+ */
+ private static String extractId(String magnetUri) {
+ List<NameValuePair> parameters = URLEncodedUtils.parse(magnetUri.substring("magnet:?".length()), Charset.forName("UTF-8"));
+ for (NameValuePair parameter : parameters) {
+ if (parameter.getName().equals("xt")) {
+ return parameter.getValue();
+ }
+ }
+ return null;
+ }
+
+ //
+ // OBJECT METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return (generateId() != null) ? generateId().hashCode() : 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof TorrentFile)) {
+ return false;
+ }
+ if (generateId() != null) {
+ return generateId().equals(((TorrentFile) object).generateId());
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return String.format("%s(%s,%s,%s)", name(), size(), magnetUri(), downloadUri());
+ }
+
+ }
+
+}
--- /dev/null
- stringBuilder.append(" Magnet: ").append(torrentFile.magnetUri()).append("\n");
- stringBuilder.append(" Download: ").append(torrentFile.downloadUri()).append("\n");
+/*
+ * Rhynodge - NewEpisodeTrigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.triggers;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.Collection;
+
+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.Predicate;
+import com.google.common.collect.Collections2;
+
+/**
+ * {@link Trigger} implementation that compares two {@link EpisodeState}s for
+ * new and changed {@link Episode}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NewEpisodeTrigger implements Trigger {
+
+ /** All new episodes. */
+ private Collection<Episode> newEpisodes;
+
+ /** All changed episodes. */
+ private Collection<Episode> changedEpisodes;
+
+ //
+ // TRIGGER METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean triggers(State currentState, State previousState) {
+ 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());
+ final EpisodeState currentEpisodeState = (EpisodeState) currentState;
+ final EpisodeState previousEpisodeState = (EpisodeState) previousState;
+
+ newEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate<Episode>() {
+
+ @Override
+ public boolean apply(Episode episode) {
+ return !previousEpisodeState.episodes().contains(episode);
+ }
+ });
+
+ changedEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate<Episode>() {
+
+ @Override
+ public boolean apply(Episode episode) {
+ if (!previousEpisodeState.episodes().contains(episode)) {
+ return false;
+ }
+
+ /* find previous episode. */
+ final Episode previousEpisode = findPreviousEpisode(episode);
+
+ /* compare the list of torrent files. */
+ Collection<TorrentFile> newTorrentFiles = Collections2.filter(episode.torrentFiles(), new Predicate<TorrentFile>() {
+
+ @Override
+ public boolean apply(TorrentFile torrentFile) {
+ return !previousEpisode.torrentFiles().contains(torrentFile);
+ }
+ });
+
+ return !newTorrentFiles.isEmpty();
+ }
+
+ private Episode findPreviousEpisode(Episode episode) {
+ for (Episode previousStateEpisode : previousEpisodeState) {
+ if (previousStateEpisode.equals(episode)) {
+ return previousStateEpisode;
+ }
+ }
+ return null;
+ }
+
+ });
+
+ 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, newEpisodes, changedEpisodes));
+ output.addText("text/html", generateHtmlText(reaction, newEpisodes, changedEpisodes));
+ return output;
+ }
+
+ //
+ // STATIC METHODS
+ //
+
+ /**
+ * Generates the plain text trigger output.
+ *
+ * @param reaction
+ * The reaction that was triggered
+ * @param newEpisodes
+ * The new episodes
+ * @param changedEpisodes
+ * The changed episodes
+ * @return The plain text output
+ */
+ private static String generatePlainText(Reaction reaction, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes) {
+ 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");
- stringBuilder.append(" Magnet: ").append(torrentFile.magnetUri()).append("\n");
- stringBuilder.append(" Download: ").append(torrentFile.downloadUri()).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");
- htmlBuilder.append("<div><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
- htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a></div>\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");
++ }
+ }
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Generates the HTML trigger output.
+ *
+ * @param reaction
+ * The reaction that was triggered
+ * @param newEpisodes
+ * The new episodes
+ * @param changedEpisodes
+ * The changed episodes
+ * @return The HTML output
+ */
+ private static String generateHtmlText(Reaction reaction, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes) {
+ StringBuilder htmlBuilder = new StringBuilder();
+ htmlBuilder.append("<html><body>\n");
+ htmlBuilder.append("<h1>").append(StringEscapeUtils.escapeHtml4(reaction.name())).append("</h1>\n");
+ if (!newEpisodes.isEmpty()) {
+ htmlBuilder.append("<h2>New Episodes</h2>\n");
+ htmlBuilder.append("<ul>\n");
+ for (Episode episode : newEpisodes) {
+ htmlBuilder.append("<li>Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("</li>\n");
+ htmlBuilder.append("<ul>\n");
+ for (TorrentFile torrentFile : episode) {
+ htmlBuilder.append("<li>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</li>\n");
+ htmlBuilder.append("<div>");
+ htmlBuilder.append("<strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong>, ");
+ htmlBuilder.append("<strong>").append(torrentFile.fileCount()).append("</strong> file(s), ");
+ htmlBuilder.append("<strong>").append(torrentFile.seedCount()).append("</strong> seed(s), ");
+ htmlBuilder.append("<strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>\n");
- htmlBuilder.append("<div><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
- htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a></div>\n");
++ htmlBuilder.append("<div>");
++ if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
++ htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
++ }
++ if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
++ htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a>");
++ }
++ htmlBuilder.append("</div>\n");
+ }
+ htmlBuilder.append("</ul>\n");
+ }
+ htmlBuilder.append("</ul>\n");
+ }
+ if (!changedEpisodes.isEmpty()) {
+ htmlBuilder.append("<h2>Changed Episodes</h2>\n");
+ htmlBuilder.append("<ul>\n");
+ for (Episode episode : changedEpisodes) {
+ htmlBuilder.append("<li>Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("</li>\n");
+ htmlBuilder.append("<ul>\n");
+ for (TorrentFile torrentFile : episode) {
+ htmlBuilder.append("<li>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</li>\n");
+ htmlBuilder.append("<div>");
+ htmlBuilder.append("<strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong>, ");
+ htmlBuilder.append("<strong>").append(torrentFile.fileCount()).append("</strong> file(s), ");
+ htmlBuilder.append("<strong>").append(torrentFile.seedCount()).append("</strong> seed(s), ");
+ htmlBuilder.append("<strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>\n");
++ htmlBuilder.append("<div>");
++ if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
++ htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Magnet</a> ");
++ }
++ if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
++ htmlBuilder.append("<a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Download</a>");
++ }
++ htmlBuilder.append("</div>\n");
+ }
+ htmlBuilder.append("</ul>\n");
+ }
+ htmlBuilder.append("</ul>\n");
+ }
+ htmlBuilder.append("</body></html>\n");
+ return htmlBuilder.toString();
+ }
+
+}
--- /dev/null
- plainText.append('\t').append(torrentFile.magnetUri()).append('\n');
- plainText.append('\t').append(torrentFile.downloadUri()).append('\n');
+/*
+ * Rhynodge - NewTorrentTrigger.java - Copyright © 2013 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.rhynodge.triggers;
+
+import static com.google.common.base.Preconditions.checkState;
+
+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.TorrentState;
+import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+
+import com.google.common.collect.Lists;
+
+/**
+ * {@link Trigger} implementation that is triggered by {@link TorrentFile}s that
+ * appear in the current {@link TorrentState} but not in the previous one.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class NewTorrentTrigger implements Trigger {
+
+ /** The newly detected torrent files. */
+ private List<TorrentFile> torrentFiles = Lists.newArrayList();
+
+ //
+ // TRIGGER METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean triggers(State currentState, State previousState) {
+ 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;
+ torrentFiles.clear();
+ for (TorrentFile torrentFile : currentTorrentState) {
+ torrentFiles.add(torrentFile);
+ }
+ for (TorrentFile torrentFile : previousTorrentState) {
+ torrentFiles.remove(torrentFile);
+ }
+ return !torrentFiles.isEmpty();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Output output(Reaction reaction) {
+ DefaultOutput output = new DefaultOutput(String.format("Found %d new Torrent(s) for “%s!”", torrentFiles.size(), reaction.name()));
+ output.addText("text/plain", getPlainTextList(torrentFiles));
+ output.addText("text/html", getHtmlTextList(torrentFiles));
+ return output;
+ }
+
+ //
+ // STATIC METHODS
+ //
+
+ /**
+ * Generates a plain text list of torrent files.
+ *
+ * @param torrentFiles
+ * The torrent files to list
+ * @return The generated plain text
+ */
+ private static String getPlainTextList(List<TorrentFile> torrentFiles) {
+ StringBuilder plainText = new StringBuilder();
+ plainText.append("New Torrents:\n\n");
+ for (TorrentFile torrentFile : torrentFiles) {
+ 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");
- htmlText.append(String.format("<div><a href=\"%s\">Magnet URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())));
- htmlText.append(String.format("<div><a href=\"%s\">Download URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())));
++ 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 torrentFiles
+ * The torrent files to list
+ * @return The generated HTML
+ */
+ private static String getHtmlTextList(List<TorrentFile> torrentFiles) {
+ StringBuilder htmlText = new StringBuilder();
+ htmlText.append("<html><body>\n");
+ htmlText.append("<h1>New Torrents</h1>\n");
+ htmlText.append("<ul>\n");
+ for (TorrentFile torrentFile : torrentFiles) {
+ htmlText.append("<li><strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</strong></li>");
+ htmlText.append("<div>Size: <strong>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</strong> in <strong>").append(torrentFile.fileCount()).append("</strong> file(s)</div>");
+ htmlText.append("<div><strong>").append(torrentFile.seedCount()).append("</strong> seed(s), <strong>").append(torrentFile.leechCount()).append("</strong> leecher(s)</div>");
++ if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
++ htmlText.append(String.format("<div><a href=\"%s\">Magnet URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())));
++ }
++ if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
++ htmlText.append(String.format("<div><a href=\"%s\">Download URI</a></div>", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())));
++ }
+ }
+ htmlText.append("</ul>\n");
+ htmlText.append("</body></html>\n");
+ return htmlText.toString();
+ }
+
+}
--- /dev/null
- "value": "reactor@reactor.de"
+ {
+ "enabled": false,
+ "name": "Example Reaction",
+
+ "query":
+ {
+ "class": "HttpQuery",
+ "parameters": [
+ {
+ "name": "url",
+ "value": "http://thepiratebay.se/search/Example%20Words/0/3/0"
+ }
+ ]
+ },
+
+ "filters": [
+ {
+ "class": "HtmlFilter"
+ },
+ {
+ "class": "PirateBayFilter"
+ }
+ ],
+
+ "trigger":
+ {
+ "class": "NewTorrentTrigger"
+ },
+
+ "action":
+ {
+ "class": "EmailAction",
+ "parameters": [
+ {
+ "name": "smtpHostname",
+ "value": "smtp"
+ },
+ {
+ "name": "sender",
++ "value": "rhynodge@rhynodge.net"
+ },
+ {
+ "name": "recipient",
+ "value": "recipient@recipient.de"
+ }
+ ]
+ },
+
+ "updateInterval": 3600
+ }