♻️ Move output generation to state
[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         /**
94          * Returns all episodes contained in this state.
95          *
96          * @return The episodes of this state
97          */
98         public Collection<Episode> episodes() {
99                 return Collections.unmodifiableCollection(episodes);
100         }
101
102         @Nonnull
103         @Override
104         protected String summary(Reaction reaction) {
105                 if (!newEpisodes.isEmpty()) {
106                         if (!changedEpisodes.isEmpty()) {
107                                 return String.format("%d new and %d changed Torrent(s) for “%s!”", newEpisodes.size(), changedEpisodes.size(), reaction.name());
108                         }
109                         return String.format("%d new Torrent(s) for “%s!”", newEpisodes.size(), reaction.name());
110                 }
111                 return String.format("%d changed Torrent(s) for “%s!”", changedEpisodes.size(), reaction.name());
112         }
113
114         @Nonnull
115         @Override
116         protected String plainText() {
117                 StringBuilder stringBuilder = new StringBuilder();
118                 if (!newEpisodes.isEmpty()) {
119                         stringBuilder.append("New Episodes\n\n");
120                         for (Episode episode : newEpisodes) {
121                                 stringBuilder.append("- ").append(episode.identifier()).append("\n");
122                                 for (TorrentFile torrentFile : episode) {
123                                         stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
124                                         if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
125                                                 stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
126                                         }
127                                         if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
128                                                 stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
129                                         }
130                                 }
131                         }
132                 }
133                 if (!changedEpisodes.isEmpty()) {
134                         stringBuilder.append("Changed Episodes\n\n");
135                         for (Episode episode : changedEpisodes) {
136                                 stringBuilder.append("- ").append(episode.identifier()).append("\n");
137                                 for (TorrentFile torrentFile : episode) {
138                                         stringBuilder.append("  - ").append(torrentFile.name()).append(", ").append(torrentFile.size()).append("\n");
139                                         if ((torrentFile.magnetUri() != null) && (torrentFile.magnetUri().length() > 0)) {
140                                                 stringBuilder.append("    Magnet: ").append(torrentFile.magnetUri()).append("\n");
141                                         }
142                                         if ((torrentFile.downloadUri() != null) && (torrentFile.downloadUri().length() > 0)) {
143                                                 stringBuilder.append("    Download: ").append(torrentFile.downloadUri()).append("\n");
144                                         }
145                                 }
146                         }
147                 }
148                 /* list all known episodes. */
149                 stringBuilder.append("All Known Episodes\n\n");
150                 ImmutableMap<Integer, Collection<Episode>> episodesBySeason = FluentIterable.from(episodes).index(Episode::season).asMap();
151                 for (Map.Entry<Integer, Collection<Episode>> seasonEntry : episodesBySeason.entrySet()) {
152                         stringBuilder.append("  Season ").append(seasonEntry.getKey()).append("\n\n");
153                         for (Episode episode : Ordering.natural().sortedCopy(seasonEntry.getValue())) {
154                                 stringBuilder.append("    Episode ").append(episode.episode()).append("\n");
155                                 for (TorrentFile torrentFile : episode) {
156                                         stringBuilder.append("      Size: ").append(torrentFile.size());
157                                         stringBuilder.append(" in ").append(torrentFile.fileCount()).append(" file(s): ");
158                                         stringBuilder.append(torrentFile.magnetUri());
159                                 }
160                         }
161                 }
162                 return stringBuilder.toString();
163         }
164
165         @Nullable
166         @Override
167         protected String htmlText() {
168                 StringBuilder htmlBuilder = new StringBuilder();
169                 htmlBuilder.append("<html><body>\n");
170                 /* show all known episodes. */
171                 htmlBuilder.append("<table>\n<caption>All Known Episodes</caption>\n");
172                 htmlBuilder.append("<thead>\n");
173                 htmlBuilder.append("<tr>");
174                 htmlBuilder.append("<th>Season</th>");
175                 htmlBuilder.append("<th>Episode</th>");
176                 htmlBuilder.append("<th>Filename</th>");
177                 htmlBuilder.append("<th>Size</th>");
178                 htmlBuilder.append("<th>File(s)</th>");
179                 htmlBuilder.append("<th>Seeds</th>");
180                 htmlBuilder.append("<th>Leechers</th>");
181                 htmlBuilder.append("<th>Magnet</th>");
182                 htmlBuilder.append("<th>Download</th>");
183                 htmlBuilder.append("</tr>\n");
184                 htmlBuilder.append("</thead>\n");
185                 htmlBuilder.append("<tbody>\n");
186                 Episode lastEpisode = null;
187                 for (Map.Entry<Integer, Collection<Episode>> seasonEntry : FluentIterable.from(Ordering.natural().reverse().sortedCopy(episodes)).index(Episode.BY_SEASON).asMap().entrySet()) {
188                         for (Episode episode : seasonEntry.getValue()) {
189                                 for (TorrentFile torrentFile : episode) {
190                                         if (newEpisodes.contains(episode)) {
191                                                 htmlBuilder.append("<tr style=\"color: #008000; font-weight: bold;\">");
192                                         } else if (newTorrentFiles.contains(torrentFile)) {
193                                                 htmlBuilder.append("<tr style=\"color: #008000;\">");
194                                         } else {
195                                                 htmlBuilder.append("<tr>");
196                                         }
197                                         if ((lastEpisode == null) || !lastEpisode.equals(episode)) {
198                                                 htmlBuilder.append("<td>").append(episode.season()).append("</td><td>").append(episode.episode()).append("</td>");
199                                         } else {
200                                                 htmlBuilder.append("<td colspan=\"2\"></td>");
201                                         }
202                                         htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.name())).append("</td>");
203                                         htmlBuilder.append("<td>").append(StringEscapeUtils.escapeHtml4(torrentFile.size())).append("</td>");
204                                         htmlBuilder.append("<td>").append(torrentFile.fileCount()).append("</td>");
205                                         htmlBuilder.append("<td>").append(torrentFile.seedCount()).append("</td>");
206                                         htmlBuilder.append("<td>").append(torrentFile.leechCount()).append("</td>");
207                                         htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.magnetUri())).append("\">Link</a></td>");
208                                         htmlBuilder.append("<td><a href=\"").append(StringEscapeUtils.escapeHtml4(torrentFile.downloadUri())).append("\">Link</a></td>");
209                                         htmlBuilder.append("</tr>\n");
210                                         lastEpisode = episode;
211                                 }
212                         }
213                 }
214                 htmlBuilder.append("</tbody>\n");
215                 htmlBuilder.append("</table>\n");
216                 htmlBuilder.append("</body></html>\n");
217                 return htmlBuilder.toString();
218         }
219
220         //
221         // ITERABLE INTERFACE
222         //
223
224         /**
225          * {@inheritDoc}
226          */
227         @Override
228         public Iterator<Episode> iterator() {
229                 return episodes.iterator();
230         }
231
232         /**
233          * {@inheritDoc}
234          */
235         @Override
236         public String toString() {
237                 return String.format("%s[episodes=%s]", getClass().getSimpleName(), episodes);
238         }
239
240         /**
241          * Stores attributes for an episode.
242          *
243          * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
244          */
245         public static class Episode implements Comparable<Episode>, Iterable<TorrentFile> {
246
247                 /** Function to extract the season of an episode. */
248                 public static final Function<Episode, Integer> BY_SEASON = new Function<Episode, Integer>() {
249
250                         @Override
251                         public Integer apply(Episode episode) {
252                                 return (episode != null ) ? episode.season() : -1;
253                         }
254                 };
255
256                 /** The season of the episode. */
257                 @JsonProperty
258                 private final int season;
259
260                 /** The number of the episode. */
261                 @JsonProperty
262                 private final int episode;
263
264                 /** The torrent files for this episode. */
265                 @JsonProperty
266                 private final List<TorrentFile> torrentFiles = new ArrayList<TorrentFile>();
267
268                 /**
269                  * No-arg constructor for deserialization.
270                  */
271                 @SuppressWarnings("unused")
272                 private Episode() {
273                         this(0, 0);
274                 }
275
276                 /**
277                  * Creates a new episode.
278                  *
279                  * @param season
280                  *            The season of the episode
281                  * @param episode
282                  *            The number of the episode
283                  */
284                 public Episode(int season, int episode) {
285                         this.season = season;
286                         this.episode = episode;
287                 }
288
289                 //
290                 // ACCESSORS
291                 //
292
293                 /**
294                  * Returns the season of this episode.
295                  *
296                  * @return The season of this episode
297                  */
298                 public int season() {
299                         return season;
300                 }
301
302                 /**
303                  * Returns the number of this episode.
304                  *
305                  * @return The number of this episode
306                  */
307                 public int episode() {
308                         return episode;
309                 }
310
311                 /**
312                  * Returns the torrent files of this episode.
313                  *
314                  * @return The torrent files of this episode
315                  */
316                 public Collection<TorrentFile> torrentFiles() {
317                         return torrentFiles;
318                 }
319
320                 /**
321                  * Returns the identifier of this episode.
322                  *
323                  * @return The identifier of this episode
324                  */
325                 public String identifier() {
326                         return String.format("S%02dE%02d", season, episode);
327                 }
328
329                 //
330                 // ACTIONS
331                 //
332
333                 /**
334                  * Adds the given torrent file to this episode.
335                  *
336                  * @param torrentFile
337                  *            The torrent file to add
338                  */
339                 public void addTorrentFile(TorrentFile torrentFile) {
340                         if (!torrentFiles.contains(torrentFile)) {
341                                 torrentFiles.add(torrentFile);
342                         }
343                 }
344
345                 //
346                 // ITERABLE METHODS
347                 //
348
349                 /**
350                  * {@inheritDoc}
351                  */
352                 @Override
353                 public Iterator<TorrentFile> iterator() {
354                         return torrentFiles.iterator();
355                 }
356
357                 /**
358                  * {@inheritDoc}
359                  */
360                 @Override
361                 public int compareTo(Episode episode) {
362                         if (season() < episode.season()) {
363                                 return -1;
364                         }
365                         if (season() > episode.season()) {
366                                 return 1;
367                         }
368                         if (episode() < episode.episode()) {
369                                 return -1;
370                         }
371                         if (episode() > episode.episode()) {
372                                 return 1;
373                         }
374                         return 0;
375                 }
376
377                 //
378                 // OBJECT METHODS
379                 //
380
381                 /**
382                  * {@inheritDoc}
383                  */
384                 @Override
385                 public int hashCode() {
386                         return season * 65536 + episode;
387                 }
388
389                 /**
390                  * {@inheritDoc}
391                  */
392                 @Override
393                 public boolean equals(Object obj) {
394                         if (!(obj instanceof Episode)) {
395                                 return false;
396                         }
397                         Episode episode = (Episode) obj;
398                         return (season == episode.season) && (this.episode == episode.episode);
399                 }
400
401                 /**
402                  * {@inheritDoc}
403                  */
404                 @Override
405                 public String toString() {
406                         return String.format("%s[season=%d,episode=%d,torrentFiles=%s]", getClass().getSimpleName(), season, episode, torrentFiles);
407                 }
408
409         }
410
411 }