abb387d38e5574705202aee491fd687e69dfc6f1
[rhynodge.git] / src / main / java / net / pterodactylus / rhynodge / states / EpisodeState.java
1 /*
2  * Rhynodge - EpisodeState.java - Copyright © 2013 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.rhynodge.states;
19
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashSet;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28 import javax.annotation.Nonnull;
29 import javax.annotation.Nullable;
30
31 import net.pterodactylus.rhynodge.Reaction;
32 import net.pterodactylus.rhynodge.State;
33 import net.pterodactylus.rhynodge.filters.EpisodeFilter;
34 import net.pterodactylus.rhynodge.states.EpisodeState.Episode;
35 import net.pterodactylus.rhynodge.states.TorrentState.TorrentFile;
36
37 import com.fasterxml.jackson.annotation.JsonProperty;
38 import com.google.common.base.Function;
39 import com.google.common.collect.FluentIterable;
40 import com.google.common.collect.ImmutableMap;
41 import com.google.common.collect.Ordering;
42 import org.apache.commons.lang3.StringEscapeUtils;
43
44 /**
45  * {@link State} implementation that stores episodes of TV shows, parsed via
46  * {@link EpisodeFilter} from a previous {@link TorrentState}.
47  *
48  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
49  */
50 public class EpisodeState extends AbstractState implements Iterable<Episode> {
51
52         /**
53          * The episodes found in the current request.
54          */
55         @JsonProperty
56         private final List<Episode> episodes = new ArrayList<>();
57         private final Set<Episode> newEpisodes = new HashSet<>();
58         private final Set<Episode> changedEpisodes = new HashSet<>();
59         private final Set<TorrentFile> newTorrentFiles = new HashSet<>();
60
61         /**
62          * No-arg constructor for deserialization.
63          */
64         @SuppressWarnings("unused")
65         private EpisodeState() {
66         }
67
68         /**
69          * Creates a new episode state.
70          *
71          * @param episodes The episodes of the request
72          */
73         public EpisodeState(Collection<Episode> episodes) {
74                 this.episodes.addAll(episodes);
75         }
76
77         public EpisodeState(Collection<Episode> episodes, Collection<Episode> newEpisodes, Collection<Episode> changedEpisodes, Collection<TorrentFile> newTorreFiles) {
78                 this(episodes);
79                 this.newEpisodes.addAll(newEpisodes);
80                 this.changedEpisodes.addAll(changedEpisodes);
81                 this.newTorrentFiles.addAll(newTorreFiles);
82         }
83
84         //
85         // ACCESSORS
86         //
87
88         @Override
89         public boolean isEmpty() {
90                 return episodes.isEmpty();
91         }
92
93         @Override
94         public boolean triggered() {
95                 return !newEpisodes.isEmpty() || !changedEpisodes.isEmpty() || !newTorrentFiles.isEmpty();
96         }
97
98         /**
99          * Returns all episodes contained in this state.
100          *
101          * @return The episodes of this state
102          */
103         public Collection<Episode> episodes() {
104                 return Collections.unmodifiableCollection(episodes);
105         }
106
107         @Nonnull
108         @Override
109         protected String summary(Reaction reaction) {
110                 if (!newEpisodes.isEmpty()) {
111                         if (!changedEpisodes.isEmpty()) {
112                                 return String.format("%d new and %d changed Torrent(s) for “%s!”", newEpisodes.size(), changedEpisodes.size(), reaction.name());
113                         }
114                         return String.format("%d new Torrent(s) for “%s!”", newEpisodes.size(), reaction.name());
115                 }
116                 return String.format("%d changed Torrent(s) for “%s!”", changedEpisodes.size(), reaction.name());
117         }
118
119         @Nonnull
120         @Override
121         protected String plainText() {
122                 StringBuilder stringBuilder = new StringBuilder();
123                 if (!newEpisodes.isEmpty()) {
124                         stringBuilder.append("New Episodes\n\n");
125                         for (Episode episode : newEpisodes) {
126                                 stringBuilder.append("- ").append(episode.identifier()).append("\n");
127                                 for (TorrentFile torrentFile : episode) {
128                                         stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
129                                         if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
130                                                 stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
131                                         }
132                                         if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
133                                                 stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
134                                         }
135                                 }
136                         }
137                 }
138                 if (!changedEpisodes.isEmpty()) {
139                         stringBuilder.append("Changed Episodes\n\n");
140                         for (Episode episode : changedEpisodes) {
141                                 stringBuilder.append("- ").append(episode.identifier()).append("\n");
142                                 for (TorrentFile torrentFile : episode) {
143                                         stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
144                                         if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
145                                                 stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
146                                         }
147                                         if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
148                                                 stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
149                                         }
150                                 }
151                         }
152                 }
153                 /* list all known episodes. */
154                 stringBuilder.append("All Known Episodes\n\n");
155                 ImmutableMap<Integer, Collection<Episode>> episodesBySeason = FluentIterable.from(episodes).index(Episode::season).asMap();
156                 for (Map.Entry<Integer, Collection<Episode>> seasonEntry : episodesBySeason.entrySet()) {
157                         stringBuilder.append("  Season ").append(seasonEntry.getKey()).append("\n\n");
158                         for (Episode episode : Ordering.natural().sortedCopy(seasonEntry.getValue())) {
159                                 stringBuilder.append("    Episode ").append(episode.episode()).append("\n");
160                                 for (TorrentFile torrentFile : episode) {
161                                         stringBuilder.append("      Size: ").append(torrentFile.size());
162                                         stringBuilder.append(" in ").append(torrentFile.fileCount()).append(" file(s): ");
163                                         stringBuilder.append(torrentFile.magnetUri());
164                                 }
165                         }
166                 }
167                 return stringBuilder.toString();
168         }
169
170         @Nullable
171         @Override
172         protected String htmlText() {
173                 StringBuilder htmlBuilder = new StringBuilder();
174                 htmlBuilder.append("<html><body>\n");
175                 /* show all known episodes. */
176                 htmlBuilder.append("<table>\n<caption>All Known Episodes</caption>\n");
177                 htmlBuilder.append("<thead>\n");
178                 htmlBuilder.append("<tr>");
179                 htmlBuilder.append("<th>Season</th>");
180                 htmlBuilder.append("<th>Episode</th>");
181                 htmlBuilder.append("<th>Filename</th>");
182                 htmlBuilder.append("<th>Size</th>");
183                 htmlBuilder.append("<th>File(s)</th>");
184                 htmlBuilder.append("<th>Seeds</th>");
185                 htmlBuilder.append("<th>Leechers</th>");
186                 htmlBuilder.append("<th>Magnet</th>");
187                 htmlBuilder.append("<th>Download</th>");
188                 htmlBuilder.append("</tr>\n");
189                 htmlBuilder.append("</thead>\n");
190                 htmlBuilder.append("<tbody>\n");
191                 Episode lastEpisode = null;
192                 for (Map.Entry<Integer, Collection<Episode>> seasonEntry : FluentIterable.from(Ordering.natural().reverse().sortedCopy(episodes)).index(Episode.BY_SEASON).asMap().entrySet()) {
193                         for (Episode episode : seasonEntry.getValue()) {
194                                 for (TorrentFile torrentFile : episode) {
195                                         if (newEpisodes.contains(episode)) {
196                                                 htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
197                                         } else if (newTorrentFiles.contains(torrentFile)) {
198                                                 htmlBuilder.append("<tr style=\"color: #008000;\">");
199                                         } else {
200                                                 htmlBuilder.append("<tr>");
201                                         }
202                                         if ((lastEpisode == null) || !lastEpisode.equals(episode)) {
203                                                 htmlBuilder.append("<td>").append(episode.season()).append("</td><td>").append(episode.episode()).append("</td>");
204                                         } else {
205                                                 htmlBuilder.append("<td colspan=\"2\"></td>");
206                                         }
207                                         htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
208                                         htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
209                                         htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
210                                         htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
211                                         htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
212                                         htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
213                                         htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
214                                         htmlBuilder.append("</tr>\n");
215                                         lastEpisode = episode;
216                                 }
217                         }
218                 }
219                 htmlBuilder.append("</tbody>\n");
220                 htmlBuilder.append("</table>\n");
221                 htmlBuilder.append("</body></html>\n");
222                 return htmlBuilder.toString();
223         }
224
225         //
226         // ITERABLE INTERFACE
227         //
228
229         /**
230          * {@inheritDoc}
231          */
232         @Override
233         public Iterator<Episode> iterator() {
234                 return episodes.iterator();
235         }
236
237         /**
238          * {@inheritDoc}
239          */
240         @Override
241         public String toString() {
242                 return String.format("%s[episodes=%s]", getClass().getSimpleName(), episodes);
243         }
244
245         /**
246          * Stores attributes for an episode.
247          *
248          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
249          */
250         public static class Episode implements Comparable<Episode>, Iterable<TorrentFile> {
251
252                 /** Function to extract the season of an episode. */
253                 public static final Function<Episode, Integer> BY_SEASON = new Function<Episode, Integer>() {
254
255                         @Override
256                         public Integer apply(Episode episode) {
257                                 return (episode != null ) ? episode.season() : -1;
258                         }
259                 };
260
261                 /** The season of the episode. */
262                 @JsonProperty
263                 private final int season;
264
265                 /** The number of the episode. */
266                 @JsonProperty
267                 private final int episode;
268
269                 /** The torrent files for this episode. */
270                 @JsonProperty
271                 private final List<TorrentFile> torrentFiles = new ArrayList<TorrentFile>();
272
273                 /**
274                  * No-arg constructor for deserialization.
275                  */
276                 @SuppressWarnings("unused")
277                 private Episode() {
278                         this(0, 0);
279                 }
280
281                 /**
282                  * Creates a new episode.
283                  *
284                  * @param season
285                  *            The season of the episode
286                  * @param episode
287                  *            The number of the episode
288                  */
289                 public Episode(int season, int episode) {
290                         this.season = season;
291                         this.episode = episode;
292                 }
293
294                 //
295                 // ACCESSORS
296                 //
297
298                 /**
299                  * Returns the season of this episode.
300                  *
301                  * @return The season of this episode
302                  */
303                 public int season() {
304                         return season;
305                 }
306
307                 /**
308                  * Returns the number of this episode.
309                  *
310                  * @return The number of this episode
311                  */
312                 public int episode() {
313                         return episode;
314                 }
315
316                 /**
317                  * Returns the torrent files of this episode.
318                  *
319                  * @return The torrent files of this episode
320                  */
321                 public Collection<TorrentFile> torrentFiles() {
322                         return torrentFiles;
323                 }
324
325                 /**
326                  * Returns the identifier of this episode.
327                  *
328                  * @return The identifier of this episode
329                  */
330                 public String identifier() {
331                         return String.format("S%02dE%02d", season, episode);
332                 }
333
334                 //
335                 // ACTIONS
336                 //
337
338                 /**
339                  * Adds the given torrent file to this episode.
340                  *
341                  * @param torrentFile
342                  *            The torrent file to add
343                  */
344                 public void addTorrentFile(TorrentFile torrentFile) {
345                         if (!torrentFiles.contains(torrentFile)) {
346                                 torrentFiles.add(torrentFile);
347                         }
348                 }
349
350                 //
351                 // ITERABLE METHODS
352                 //
353
354                 /**
355                  * {@inheritDoc}
356                  */
357                 @Override
358                 public Iterator<TorrentFile> iterator() {
359                         return torrentFiles.iterator();
360                 }
361
362                 /**
363                  * {@inheritDoc}
364                  */
365                 @Override
366                 public int compareTo(Episode episode) {
367                         if (season() < episode.season()) {
368                                 return -1;
369                         }
370                         if (season() > episode.season()) {
371                                 return 1;
372                         }
373                         if (episode() < episode.episode()) {
374                                 return -1;
375                         }
376                         if (episode() > episode.episode()) {
377                                 return 1;
378                         }
379                         return 0;
380                 }
381
382                 //
383                 // OBJECT METHODS
384                 //
385
386                 /**
387                  * {@inheritDoc}
388                  */
389                 @Override
390                 public int hashCode() {
391                         return season * 65536 + episode;
392                 }
393
394                 /**
395                  * {@inheritDoc}
396                  */
397                 @Override
398                 public boolean equals(Object obj) {
399                         if (!(obj instanceof Episode)) {
400                                 return false;
401                         }
402                         Episode episode = (Episode) obj;
403                         return (season == episode.season) && (this.episode == episode.episode);
404                 }
405
406                 /**
407                  * {@inheritDoc}
408                  */
409                 @Override
410                 public String toString() {
411                         return String.format("%s[season=%d,episode=%d,torrentFiles=%s]", getClass().getSimpleName(), season, episode, torrentFiles);
412                 }
413
414         }
415
416 }