From: David ‘Bombe’ Roden Date: Thu, 10 Jan 2013 21:24:55 +0000 (+0100) Subject: Merge branch 'master' into rewrite X-Git-Tag: 0.1~38 X-Git-Url: https://git.pterodactylus.net/?p=rhynodge.git;a=commitdiff_plain;h=6ec36ef950c23c135bf0e112d932c5b7068189b8 Merge branch 'master' into rewrite Conflicts: src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java --- 6ec36ef950c23c135bf0e112d932c5b7068189b8 diff --cc src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java index 11865e7,0000000..c368bf3 mode 100644,000000..100644 --- a/src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java +++ b/src/main/java/net/pterodactylus/rhynodge/filters/KickAssTorrentsFilter.java @@@ -1,168 -1,0 +1,106 @@@ +/* + * 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 . + */ + +package net.pterodactylus.rhynodge.filters; + - import static com.google.common.base.Preconditions.checkState; - - import java.net.URI; - import java.net.URISyntaxException; - +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 David ‘Bombe’ Roden + */ - public class KickAssTorrentsFilter implements Filter { ++public class KickAssTorrentsFilter extends TorrentSiteFilter { ++ ++ // ++ // TORRENTSITEFILTER METHODS ++ // + + /** + * {@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(); - 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; ++ protected Elements getDataRows(Document document) { ++ return document.select("table.data").select("tr:gt(0)"); + } + - // - // STATIC METHODS - // - + /** - * Extracts the name from the given row. - * - * @param dataRow - * The row to extract the name from - * @return The extracted name ++ * {@inheritDoc} + */ - private static String extractName(Element dataRow) { ++ @Override ++ protected String extractName(Element dataRow) { + return dataRow.select("div.torrentname a.normalgrey").text(); + } + + /** - * Extracts the size from the given row. - * - * @param dataRow - * The row to extract the size from - * @return The extracted size ++ * {@inheritDoc} + */ - private static String extractSize(Element dataRow) { ++ @Override ++ protected String extractSize(Element dataRow) { + return dataRow.select("td:eq(1)").text(); + } + + /** - * Extracts the magnet URI from the given row. - * - * @param dataRow - * The row to extract the magnet URI from - * @return The extracted magnet URI ++ * {@inheritDoc} + */ - private static String extractMagnetUri(Element dataRow) { ++ @Override ++ protected String extractMagnetUri(Element dataRow) { + return dataRow.select("a.imagnet").attr("href"); + } + + /** - * Extracts the download URI from the given row. - * - * @param dataRow - * The row to extract the download URI from - * @return The extracted download URI ++ * {@inheritDoc} + */ - private static String extractDownloadUri(Element dataRow) { ++ @Override ++ protected String extractDownloadUri(Element dataRow) { + return dataRow.select("a.idownload:not(.partner1Button)").attr("href"); + } + + /** - * Extracts the file count from the given row. - * - * @param dataRow - * The row to extract the file count from - * @return The extracted file count ++ * {@inheritDoc} + */ - private static int extractFileCount(Element dataRow) { ++ @Override ++ protected int extractFileCount(Element dataRow) { + return Integer.valueOf(dataRow.select("td:eq(2)").text()); + } + + /** - * Extracts the seed count from the given row. - * - * @param dataRow - * The row to extract the seed count from - * @return The extracted seed count ++ * {@inheritDoc} + */ - private static int extractSeedCount(Element dataRow) { ++ @Override ++ protected int extractSeedCount(Element dataRow) { + return Integer.valueOf(dataRow.select("td:eq(4)").text()); + } + + /** - * Extracts the leech count from the given row. - * - * @param dataRow - * The row to extract the leech count from - * @return The extracted leech count ++ * {@inheritDoc} + */ - private static int extractLeechCount(Element dataRow) { ++ @Override ++ protected int extractLeechCount(Element dataRow) { + return Integer.valueOf(dataRow.select("td:eq(5)").text()); + } + +} diff --cc src/main/java/net/pterodactylus/rhynodge/filters/PirateBayFilter.java index 0000000,0000000..45c33c1 new file mode 100644 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/filters/PirateBayFilter.java @@@ -1,0 -1,0 +1,98 @@@ ++/* ++ * 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 . ++ */ ++ ++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 David ‘Bombe’ Roden ++ */ ++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()); ++ } ++ ++} diff --cc src/main/java/net/pterodactylus/rhynodge/filters/TorrentSiteFilter.java index 0000000,0000000..b3b5691 new file mode 100644 --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/filters/TorrentSiteFilter.java @@@ -1,0 -1,0 +1,167 @@@ ++/* ++ * 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 . ++ */ ++ ++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 David ‘Bombe’ Roden ++ */ ++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); ++ ++} diff --cc src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java index ce5b06f,0000000..c4c3e0b mode 100644,000000..100644 --- a/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java +++ b/src/main/java/net/pterodactylus/rhynodge/states/TorrentState.java @@@ -1,305 -1,0 +1,307 @@@ +/* + * 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 . + */ + +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 David ‘Bombe’ Roden + */ +public class TorrentState extends AbstractState implements Iterable { + + /** The torrent files. */ + @JsonProperty + private List 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 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 David ‘Bombe’ Roden + */ + 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 magnet 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 ++ * @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 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()); + } + + } + +} diff --cc src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java index f23869d,0000000..a8e321c mode 100644,000000..100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewEpisodeTrigger.java @@@ -1,233 -1,0 +1,253 @@@ +/* + * 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 . + */ + +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 David ‘Bombe’ Roden + */ +public class NewEpisodeTrigger implements Trigger { + + /** All new episodes. */ + private Collection newEpisodes; + + /** All changed episodes. */ + private Collection 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() { + + @Override + public boolean apply(Episode episode) { + return !previousEpisodeState.episodes().contains(episode); + } + }); + + changedEpisodes = Collections2.filter(currentEpisodeState.episodes(), new Predicate() { + + @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 newTorrentFiles = Collections2.filter(episode.torrentFiles(), new Predicate() { + + @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 newEpisodes, Collection 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"); - 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"); ++ } + } + } + } + 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 newEpisodes, Collection changedEpisodes) { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append("\n"); + htmlBuilder.append("

").append(StringEscapeUtils.escapeHtml4(reaction.name())).append("

\n"); + if (!newEpisodes.isEmpty()) { + htmlBuilder.append("

New Episodes

\n"); + htmlBuilder.append("
    \n"); + for (Episode episode : newEpisodes) { + htmlBuilder.append("
  • Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("
  • \n"); + htmlBuilder.append("
      \n"); + for (TorrentFile torrentFile : episode) { + htmlBuilder.append("
    • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
    • \n"); + htmlBuilder.append("
      "); + htmlBuilder.append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(", "); + htmlBuilder.append("").append(torrentFile.fileCount()).append(" file(s), "); + htmlBuilder.append("").append(torrentFile.seedCount()).append(" seed(s), "); + htmlBuilder.append("").append(torrentFile.leechCount()).append(" leecher(s)
      \n"); - htmlBuilder.append("
      Magnet "); - htmlBuilder.append("Download
      \n"); ++ htmlBuilder.append("
      "); ++ if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { ++ htmlBuilder.append("Magnet "); ++ } ++ if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { ++ htmlBuilder.append("Download"); ++ } ++ htmlBuilder.append("
      \n"); + } + htmlBuilder.append("
    \n"); + } + htmlBuilder.append("
\n"); + } + if (!changedEpisodes.isEmpty()) { + htmlBuilder.append("

Changed Episodes

\n"); + htmlBuilder.append("
    \n"); + for (Episode episode : changedEpisodes) { + htmlBuilder.append("
  • Season ").append(episode.season()).append(", Episode ").append(episode.episode()).append("
  • \n"); + htmlBuilder.append("
      \n"); + for (TorrentFile torrentFile : episode) { + htmlBuilder.append("
    • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
    • \n"); + htmlBuilder.append("
      "); + htmlBuilder.append("").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(", "); + htmlBuilder.append("").append(torrentFile.fileCount()).append(" file(s), "); + htmlBuilder.append("").append(torrentFile.seedCount()).append(" seed(s), "); + htmlBuilder.append("").append(torrentFile.leechCount()).append(" leecher(s)
      \n"); - htmlBuilder.append("
      Magnet "); - htmlBuilder.append("Download
      \n"); ++ htmlBuilder.append("
      "); ++ if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { ++ htmlBuilder.append("Magnet "); ++ } ++ if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { ++ htmlBuilder.append("Download"); ++ } ++ htmlBuilder.append("
      \n"); + } + htmlBuilder.append("
    \n"); + } + htmlBuilder.append("
\n"); + } + htmlBuilder.append("\n"); + return htmlBuilder.toString(); + } + +} diff --cc src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java index a21a8d6,0000000..2803bb5 mode 100644,000000..100644 --- a/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java +++ b/src/main/java/net/pterodactylus/rhynodge/triggers/NewTorrentTrigger.java @@@ -1,130 -1,0 +1,138 @@@ +/* + * 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 . + */ + +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 David ‘Bombe’ Roden + */ +public class NewTorrentTrigger implements Trigger { + + /** The newly detected torrent files. */ + private List 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 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"); - plainText.append('\t').append(torrentFile.magnetUri()).append('\n'); - plainText.append('\t').append(torrentFile.downloadUri()).append('\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 torrentFiles + * The torrent files to list + * @return The generated HTML + */ + private static String getHtmlTextList(List torrentFiles) { + StringBuilder htmlText = new StringBuilder(); + htmlText.append("\n"); + htmlText.append("

New Torrents

\n"); + htmlText.append("
    \n"); + for (TorrentFile torrentFile : torrentFiles) { + htmlText.append("
  • ").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("
  • "); + htmlText.append("
    Size: ").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append(" in ").append(torrentFile.fileCount()).append(" file(s)
    "); + htmlText.append("
    ").append(torrentFile.seedCount()).append(" seed(s), ").append(torrentFile.leechCount()).append(" leecher(s)
    "); - htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri()))); - htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri()))); ++ if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) { ++ htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.magnetUri()))); ++ } ++ if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) { ++ htmlText.append(String.format("", StringEscapeUtils.escapeHtml4(torrentFile.downloadUri()))); ++ } + } + htmlText.append("
\n"); + htmlText.append("\n"); + return htmlText.toString(); + } + +} diff --cc src/main/resources/chains/thepiratebay-example.json index 0000000,f2eed53..ad79213 mode 000000,100644..100644 --- a/src/main/resources/chains/thepiratebay-example.json +++ b/src/main/resources/chains/thepiratebay-example.json @@@ -1,0 -1,50 +1,50 @@@ + { + "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": "reactor@reactor.de" ++ "value": "rhynodge@rhynodge.net" + }, + { + "name": "recipient", + "value": "recipient@recipient.de" + } + ] + }, + + "updateInterval": 3600 + }