/
Episode.java
328 lines (287 loc) · 10.9 KB
/
Episode.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/** Copyright 2012, 2013 Kevin Hausmann
*
* This file is part of PodCatcher Deluxe.
*
* PodCatcher Deluxe 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.
*
* PodCatcher Deluxe 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 PodCatcher Deluxe. If not, see <http://www.gnu.org/licenses/>.
*/
package net.alliknow.podcatcher.model.types;
import android.text.Html;
import android.util.Log;
import net.alliknow.podcatcher.model.ParserUtils;
import net.alliknow.podcatcher.model.tags.RSS;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* The episode type. Each episode represents an item from a podcast's RSS/XML
* feed. Episodes are created when the podcast is loaded (parsed), you should
* have no need to create instances yourself.
*/
public class Episode implements Comparable<Episode> {
/** The podcast this episode is part of */
private Podcast podcast;
/**
* The index (starting with zero at the top of the feed) this episode is in
* its podcast. -1 means that we do not have this information.
*/
private final int index;
/** This episode title */
private String name;
/** The episode's online location */
private URL mediaUrl;
/** The episode's release date */
private Date pubDate;
/** The episode duration */
private int duration = -1;
/** The episode's description */
private String description;
/** The episode's long content description */
private String content;
/**
* Create a new episode.
*
* @param podcast Podcast this episode belongs to. Cannot be
* <code>null</code>.
* @param index The index of the episode created in the podcast's feed (used
* for sorting if the publication date is not available).
*/
public Episode(Podcast podcast, int index) {
if (podcast == null)
throw new NullPointerException("Episode can not have null as the podcast instance!");
this.podcast = podcast;
this.index = index;
}
/**
* Create a new episode and set all fields manually.
*
* @param podcast Podcast this episode belongs to. Cannot be
* <code>null</code>.
* @param name Episode name.
* @param mediaUrl The remote URL of this episode.
* @param pubDate The publication date.
* @param description The episode's description.
*/
public Episode(Podcast podcast, String name, URL mediaUrl, Date pubDate, String description) {
this(podcast, -1);
this.name = name;
this.mediaUrl = mediaUrl;
this.description = description;
// Publication date might not be present
if (pubDate != null)
this.pubDate = new Date(pubDate.getTime());
}
/**
* @return The owning podcast. This will not be <code>null</code>.
*/
public Podcast getPodcast() {
return podcast;
}
/**
* @return The index for this episode object in the podcast's feed. -1 means
* that this information is not available.
*/
public int getPositionInPodcast() {
return index;
}
/**
* @return The episode's title.
*/
public String getName() {
return name;
}
/**
* @return The media content online location.
*/
public URL getMediaUrl() {
return mediaUrl;
}
/**
* @return The publication date for this episode.
*/
public Date getPubDate() {
if (pubDate == null)
return null;
else
return new Date(pubDate.getTime());
}
/**
* @return The episode's duration as given by the podcast feed converted
* into a string 00:00:00. This might not be available and therefore
* <code>null</code>.
*/
public String getDurationString() {
return duration > 0 ? ParserUtils.formatTime(duration) : null;
}
/**
* @return The episode's duration as given by the podcast feed or -1 if not
* available.
*/
public int getDuration() {
return duration;
}
/**
* @return The description for this episode (if any). Might be
* <code>null</code>.
*/
public String getDescription() {
return description;
}
/**
* @return The long content description for this episode from the
* content:encoded tag (if any). Might be <code>null</code>.
*/
public String getLongDescription() {
return content;
}
@Override
public String toString() {
return getName();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
else if (!(o instanceof Episode))
return false;
Episode other = (Episode) o;
if (mediaUrl == null || other.getMediaUrl() == null)
return false;
else
return mediaUrl.toString().equals(((Episode) o).getMediaUrl().toString());
}
@Override
public int hashCode() {
return mediaUrl == null ? 0 : mediaUrl.toString().hashCode();
}
@Override
public int compareTo(Episode another) {
// We need to be "consistent with equals": only return 0 (zero) for
// equal episodes. Failing to do so will cause episodes with equal
// pubDates to mysteriously disappear when put in a SortedSet.
if (this.pubDate == null && another.getPubDate() == null) {
// This should never be zero unless the episodes are equal, since a
// podcast might publish two episodes at the same pubDate. If it is
// (and the episode are not equal) we use the original order from
// the feed instead.
return this.equals(another) ? 0 : index - another.getPositionInPodcast();
}
else if (this.pubDate == null && another.getPubDate() != null)
return -1;
else if (this.pubDate != null && another.getPubDate() == null)
return 1;
else {
final int result = pubDate.compareTo(another.getPubDate());
// This should never be zero unless the episodes are equal, since a
// podcast might publish two episodes at the same pubDate. If it is
// (and the episode are not equal) we use the original order from
// the feed instead.
if (result == 0)
return this.equals(another) ? 0 : index - another.getPositionInPodcast();
else
return -1 * result;
}
}
/**
* Read data from an item node in the RSS/XML podcast file and use it to set
* this episode's fields.
*
* @param parser Podcast file parser, set to the start tag of the item to
* read.
* @throws XmlPullParserException On parsing problems.
* @throws IOException On I/O problems.
*/
void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
// Make sure we start at item tag
parser.require(XmlPullParser.START_TAG, "", RSS.ITEM);
// Look at all start tags of this item
while (parser.nextTag() == XmlPullParser.START_TAG) {
final String tagName = parser.getName();
// Episode title
if (tagName.equalsIgnoreCase(RSS.TITLE))
name = Html.fromHtml(parser.nextText().trim()).toString();
// Episode media URL
else if (tagName.equalsIgnoreCase(RSS.ENCLOSURE)) {
mediaUrl = createMediaUrl(parser.getAttributeValue("", RSS.URL));
parser.nextText();
}
// Episode publication date (2 options)
else if (tagName.equalsIgnoreCase(RSS.DATE) && pubDate == null)
pubDate = parsePubDate(parser.nextText());
else if (tagName.equalsIgnoreCase(RSS.PUBDATE) && pubDate == null)
pubDate = parsePubDate(parser.nextText());
// Episode duration
else if (tagName.equalsIgnoreCase(RSS.DURATION))
duration = parseDuration(parser.nextText());
// Episode description
else if (tagName.equalsIgnoreCase(RSS.DESCRIPTION))
description = parser.nextText();
else if (isContentEncodedTag(parser))
content = parser.nextText();
// Unneeded node, skip...
else
ParserUtils.skipSubTree(parser);
}
// Make sure we end at item tag
parser.require(XmlPullParser.END_TAG, "", RSS.ITEM);
}
private boolean isContentEncodedTag(XmlPullParser parser) {
return RSS.CONTENT_ENCODED.equals(parser.getName()) &&
RSS.CONTENT_NAMESPACE.equals(parser.getNamespace(parser.getPrefix()));
}
private URL createMediaUrl(String url) {
try {
return new URL(url);
} catch (MalformedURLException e) {
Log.e(getClass().getSimpleName(), "Episode has invalid URL", e);
}
return null;
}
private Date parsePubDate(String value) {
try {
// RSS/XML files use this format for dates
DateFormat formatter =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
return formatter.parse(value);
} catch (ParseException e) {
Log.w(getClass().getSimpleName(), "Episode has invalid publication date", e);
}
return null;
}
private int parseDuration(String durationString) {
try {
// Duration simply given as number of seconds
return Integer.parseInt(durationString);
} catch (NumberFormatException e) {
// The duration is given as something like "1:12:34" instead
try {
final String[] split = durationString.split(":");
if (split.length == 2)
return Integer.parseInt(split[1]) + Integer.parseInt(split[0]) * 60;
else if (split.length == 3)
return Integer.parseInt(split[2]) + Integer.parseInt(split[1]) * 60
+ Integer.parseInt(split[0]) * 3600;
else
return -1;
} catch (NumberFormatException ex) {
return -1;
}
}
}
}