├── settings.gradle
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src
├── main
│ └── java
│ │ └── com
│ │ └── icosillion
│ │ └── podengine
│ │ ├── exceptions
│ │ ├── DateFormatException.java
│ │ ├── InvalidFeedException.java
│ │ └── MalformedFeedException.java
│ │ ├── models
│ │ ├── ITunesOwner.java
│ │ ├── TextInputInfo.java
│ │ ├── CloudInfo.java
│ │ ├── ITunesChannelInfo.java
│ │ ├── ITunesInfo.java
│ │ ├── ITunesItemInfo.java
│ │ ├── Episode.java
│ │ └── Podcast.java
│ │ └── utils
│ │ └── DateUtils.java
└── test
│ └── java
│ └── com
│ └── icosillion
│ └── podengine
│ ├── models
│ ├── NetworkTest.java
│ ├── EpisodeTest.java
│ └── PodcastOverviewTest.java
│ └── utils
│ └── DateUtilsTest.java
├── .github
└── workflows
│ └── test.yml
├── LICENSE
├── README.md
├── gradlew.bat
├── feed.rss
└── gradlew
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'podengine'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | .idea/
3 | target/
4 | build/
5 | gradle.properties
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MarkusLewis/Podcast-Feed-Library/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/exceptions/DateFormatException.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.exceptions;
2 |
3 | public class DateFormatException extends Exception {
4 |
5 | public DateFormatException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/exceptions/InvalidFeedException.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.exceptions;
2 |
3 | public class InvalidFeedException extends Exception {
4 |
5 | public InvalidFeedException(String message, Throwable throwable) {
6 | super(message, throwable);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: Set up JDK 1.8
12 | uses: actions/setup-java@v1
13 | with:
14 | java-version: 1.8
15 | - name: Test with Gradle
16 | run: ./gradlew test
17 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/exceptions/MalformedFeedException.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.exceptions;
2 |
3 | public class MalformedFeedException extends Exception {
4 |
5 | public MalformedFeedException(String message) {
6 | super(message);
7 | }
8 |
9 | public MalformedFeedException(String message, Throwable throwable) {
10 | super(message, throwable);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/java/com/icosillion/podengine/models/NetworkTest.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import com.icosillion.podengine.exceptions.InvalidFeedException;
4 | import com.icosillion.podengine.exceptions.MalformedFeedException;
5 | import org.junit.Test;
6 |
7 | import java.net.MalformedURLException;
8 | import java.net.URL;
9 |
10 | import static org.junit.Assert.assertEquals;
11 | import static org.junit.Assert.fail;
12 |
13 | /**
14 | * Podcast unit test class.
15 | */
16 | public class NetworkTest {
17 |
18 | @Test
19 | public void testRelayFeed() {
20 | try {
21 | Podcast podcast = new Podcast(new URL("https://www.relay.fm/master/feed"));
22 | assertEquals("Relay FM Master Feed", podcast.getTitle());
23 | } catch (InvalidFeedException | MalformedURLException | MalformedFeedException e) {
24 | fail(e.getMessage());
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 Icosillion Ltd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8 | persons to whom the Software is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11 | Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PodEngine 2.4.1 – Podcast Feed Library
2 |
3 | ## Java library for parsing your podcast feeds 🚀
4 | * Written in Java 7 🤖
5 | * Thoroughly tested 🕹️
6 | * Parses iTunes-specific tags 🎵
7 | * Handles all RSS attributes 💪
8 | * MIT Licensed (Use it for all your commercial things!) 🤑
9 |
10 | ## Installation 📦
11 | ### Gradle (Groovy)
12 | ```groovy
13 | repositories {
14 | mavenCentral()
15 | }
16 |
17 | dependencies {
18 | implementation group: 'com.icosillion.podengine', name: 'podengine', version:'2.4.1'
19 | }
20 | ```
21 |
22 | ### Maven
23 | ```xml
24 |
25 |
26 | com.icosillion.podengine
27 | podengine
28 | 2.4.1
29 |
30 |
31 | ```
32 |
33 | ## Getting Started 🌱
34 | ### Reading your feed
35 | ```java
36 | //Download and parse the Cortex RSS feed
37 | Podcast podcast = new Podcast(new URL("https://www.relay.fm/cortex/feed"));
38 |
39 | //Display Feed Details
40 | System.out.printf("💼 %s has %d episodes!\n", podcast.getTitle(), podcast.getEpisodes().size());
41 |
42 | //List all episodes
43 | for (Episode episode : episodes) {
44 | System.out.println("- " + episode.getTitle());
45 | }
46 | ```
47 |
48 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/ITunesOwner.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import org.dom4j.Element;
4 | import org.dom4j.Namespace;
5 | import org.dom4j.QName;
6 |
7 | public class ITunesOwner {
8 |
9 | private final Element ownerElement;
10 | private final Namespace iTunesNamespace;
11 | private String name, email;
12 |
13 | public ITunesOwner(Element ownerElement) {
14 | this.ownerElement = ownerElement;
15 | this.iTunesNamespace = this.ownerElement.getNamespaceForPrefix("itunes");
16 | }
17 |
18 | public String getName() {
19 | if (this.name != null) {
20 | return this.name;
21 | }
22 |
23 | Element nameElement = this.ownerElement.element(QName.get("name", this.iTunesNamespace));
24 | if (nameElement == null) {
25 | return null;
26 | }
27 |
28 | return this.name = nameElement.getText();
29 | }
30 |
31 | public String getEmail() {
32 | if (this.email != null) {
33 | return this.email;
34 | }
35 |
36 | Element emailElement = this.ownerElement.element(QName.get("email", this.iTunesNamespace));
37 | if (emailElement == null) {
38 | return null;
39 | }
40 |
41 | return this.email = emailElement.getText();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/utils/DateUtils.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.utils;
2 |
3 | import java.text.ParseException;
4 | import java.text.SimpleDateFormat;
5 | import java.util.Date;
6 | import java.util.Locale;
7 |
8 | public class DateUtils {
9 |
10 | public static Date stringToDate(String dt) {
11 | SimpleDateFormat[] dateFormats = {
12 | new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US),
13 | new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z", Locale.US),
14 | new SimpleDateFormat("EEE, dd MMM yyyy HH:mm Z", Locale.US),
15 | new SimpleDateFormat("dd MMM yyyy HH:mm Z", Locale.US)
16 | };
17 |
18 | String normalizedDt = normalize(dt);
19 |
20 | Date date = null;
21 |
22 | for (SimpleDateFormat dateFormat : dateFormats) {
23 | try {
24 | date = dateFormat.parse(normalizedDt);
25 | break;
26 | } catch (ParseException e) {
27 | //This format didn't work, keep going
28 | }
29 | }
30 |
31 | return date;
32 | }
33 |
34 | private static String normalize(String dt) {
35 | return dt.replace("Tues,", "Tue,")
36 | .replace("Thurs,", "Thu,")
37 | .replace("Wednes,", "Wed,");
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/test/java/com/icosillion/podengine/utils/DateUtilsTest.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.utils;
2 |
3 | import org.junit.Test;
4 |
5 | import java.text.ParseException;
6 | import java.text.SimpleDateFormat;
7 | import java.util.Date;
8 | import java.util.Locale;
9 | import java.util.TimeZone;
10 |
11 | import static org.junit.Assert.assertEquals;
12 | import static org.junit.Assert.assertNotNull;
13 | import static org.junit.Assert.fail;
14 |
15 | public class DateUtilsTest {
16 |
17 | private static final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
18 |
19 | @Test
20 | public void parsesStandardDateFormats() {
21 | String dateTime = "Tue, 03 March 2009 15:00:00 -0000";
22 |
23 | Date date = DateUtils.stringToDate(dateTime);
24 |
25 | sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
26 |
27 | assertNotNull(date);
28 | assertEquals("Tue, 03 Mar 2009 15:00:00 +0000", sdf.format(date));
29 | }
30 |
31 | @Test
32 | public void parsesNonStandardDateFormats() {
33 | try {
34 | assertEquals(
35 | sdf.parse("Tue, 03 Mar 2009 16:00:00 +0100"),
36 | DateUtils.stringToDate("Tues, 03 March 2009 15:00:00 -0000")
37 | );
38 |
39 | assertEquals(
40 | sdf.parse("Wed, 04 Mar 2009 16:00:00 +0100"),
41 | DateUtils.stringToDate("Wednes, 04 March 2009 15:00:00 -0000")
42 | );
43 |
44 | assertEquals(
45 | sdf.parse("Thu, 05 Mar 2009 16:00:00 +0100"),
46 | DateUtils.stringToDate("Thurs, 05 March 2009 15:00:00 -0000")
47 | );
48 | } catch (ParseException ex) {
49 | fail("Failed to parse date: " + ex.getMessage());
50 | }
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/TextInputInfo.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import org.dom4j.Element;
4 |
5 | import java.net.MalformedURLException;
6 | import java.net.URL;
7 |
8 | public class TextInputInfo {
9 |
10 | private final Element textInputElement;
11 |
12 | //Caching
13 | private String title, description, name;
14 | private URL link;
15 |
16 | //TODO Throw error if required subelement is missing?
17 |
18 | public TextInputInfo(Element textInputElement) {
19 | this.textInputElement = textInputElement;
20 | }
21 |
22 | public String getTitle() {
23 | if (this.title != null) {
24 | return this.title;
25 | }
26 |
27 | Element titleElement = this.textInputElement.element("title");
28 | if (titleElement == null) {
29 | return null;
30 | }
31 |
32 | return this.title = titleElement.getText();
33 | }
34 |
35 | public String getDescription() {
36 | if (this.description != null) {
37 | return this.description;
38 | }
39 |
40 | Element descriptionElement = this.textInputElement.element("description");
41 | if (descriptionElement == null) {
42 | return null;
43 | }
44 |
45 | return this.description = descriptionElement.getText();
46 | }
47 |
48 | public String getName() {
49 | if (this.name != null) {
50 | return this.name;
51 | }
52 |
53 | Element nameElement = this.textInputElement.element("name");
54 | if (nameElement == null) {
55 | return null;
56 | }
57 |
58 | return this.name = nameElement.getText();
59 | }
60 |
61 | public URL getLink() throws MalformedURLException {
62 | if (this.link != null) {
63 | return this.link;
64 | }
65 |
66 | Element linkElement = this.textInputElement.element("link");
67 | if (linkElement == null) {
68 | return null;
69 | }
70 |
71 | return this.link = new URL(linkElement.getTextTrim());
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/CloudInfo.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import org.dom4j.Attribute;
4 | import org.dom4j.Element;
5 |
6 | public class CloudInfo {
7 |
8 | private final Element cloudElement;
9 |
10 | //Caching
11 | private String domain, path, registerProcedure, protocol;
12 | private Integer port;
13 |
14 | public CloudInfo(Element cloudElement) {
15 | this.cloudElement = cloudElement;
16 | }
17 |
18 | public String getDomain() {
19 | if (this.domain != null) {
20 | return this.domain;
21 | }
22 |
23 | Attribute domainAttribute = cloudElement.attribute("domain");
24 | if (domainAttribute == null) {
25 | return null;
26 | }
27 |
28 | return this.domain = domainAttribute.getValue();
29 | }
30 |
31 | public Integer getPort() {
32 | if (this.port != null) {
33 | return this.port;
34 | }
35 |
36 | Attribute portAttribute = cloudElement.attribute("port");
37 | if (portAttribute == null) {
38 | return null;
39 | }
40 |
41 | try {
42 | this.port = Integer.valueOf(portAttribute.getValue());
43 | } catch (NumberFormatException e) {
44 | //TODO Should this return an exception?
45 | return null;
46 | }
47 |
48 | return this.port;
49 | }
50 |
51 | public String getPath() {
52 | if (this.path != null) {
53 | return this.path;
54 | }
55 |
56 | Attribute pathAttribute = cloudElement.attribute("path");
57 | if (pathAttribute == null) {
58 | return null;
59 | }
60 |
61 | return this.path = pathAttribute.getValue();
62 | }
63 |
64 | public String getRegisterProcedure() {
65 | if (this.registerProcedure != null) {
66 | return this.registerProcedure;
67 | }
68 |
69 | Attribute registerProcedureAttribute = cloudElement.attribute("registerProcedure");
70 | if (registerProcedureAttribute == null) {
71 | return null;
72 | }
73 |
74 | return this.registerProcedure = registerProcedureAttribute.getValue();
75 | }
76 |
77 | public String getProtocol() {
78 | if (this.protocol != null) {
79 | return this.protocol;
80 | }
81 |
82 | Attribute protocolAttribute = cloudElement.attribute("protocol");
83 | if (protocolAttribute == null) {
84 | return null;
85 | }
86 |
87 | return this.protocol = protocolAttribute.getValue();
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/ITunesChannelInfo.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import org.dom4j.Element;
4 | import org.dom4j.QName;
5 |
6 | import java.net.MalformedURLException;
7 | import java.net.URL;
8 |
9 | public class ITunesChannelInfo extends ITunesInfo {
10 |
11 | public enum FeedType {
12 | EPISODIC, SERIAL
13 | }
14 |
15 | //TODO Category
16 | private Boolean complete;
17 | private URL newFeedURL;
18 | private ITunesOwner owner;
19 | private FeedType type;
20 |
21 | public ITunesChannelInfo(Element parent) {
22 | super(parent);
23 | }
24 |
25 | public boolean isComplete() {
26 | if (this.complete != null) {
27 | return this.complete;
28 | }
29 |
30 | Element completeElement = this.parent.element(QName.get("complete", this.iTunesNamespace));
31 | if (completeElement == null) {
32 | return this.complete = false;
33 | }
34 |
35 | if ("yes".equalsIgnoreCase(completeElement.getTextTrim())) {
36 | return this.complete = true;
37 | }
38 |
39 | return this.complete = false;
40 | }
41 |
42 | public URL getNewFeedURL() throws MalformedURLException {
43 | if (this.newFeedURL != null) {
44 | return this.newFeedURL;
45 | }
46 |
47 | Element newFeedURLElement = this.parent.element(QName.get("new-feed-url", this.iTunesNamespace));
48 | if (newFeedURLElement == null) {
49 | return null;
50 | }
51 |
52 | return this.newFeedURL = new URL(newFeedURLElement.getTextTrim());
53 | }
54 |
55 | public ITunesOwner getOwner() {
56 | if (this.owner != null) {
57 | return this.owner;
58 | }
59 |
60 | Element ownerElement = this.parent.element(QName.get("owner", this.iTunesNamespace));
61 | if (ownerElement == null) {
62 | return null;
63 | }
64 |
65 | return this.owner = new ITunesOwner(ownerElement);
66 | }
67 |
68 | public FeedType getType() {
69 | if (this.type != null) {
70 | return this.type;
71 | }
72 |
73 | Element typeElement = this.parent.element(QName.get("type", this.iTunesNamespace));
74 | if (typeElement == null) {
75 | return this.type = FeedType.EPISODIC;
76 | }
77 |
78 | String rawType = typeElement.getTextTrim().toLowerCase();
79 |
80 | if (rawType.equals("episodic")) {
81 | this.type = FeedType.EPISODIC;
82 | } else if (rawType.equals("serial")) {
83 | this.type = FeedType.SERIAL;
84 | }
85 |
86 | return this.type;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/ITunesInfo.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import org.dom4j.Element;
4 | import org.dom4j.Namespace;
5 | import org.dom4j.QName;
6 |
7 | import java.net.MalformedURLException;
8 | import java.net.URL;
9 |
10 | public abstract class ITunesInfo {
11 |
12 | public enum ExplicitLevel {
13 | EXPLICIT, CLEAN, NO, UNKNOWN
14 | }
15 |
16 | protected final Element parent;
17 | protected final Namespace iTunesNamespace;
18 | private String author, subtitle, summary, imageString;
19 | private ExplicitLevel explicit;
20 | private Boolean block;
21 | private URL image;
22 |
23 | public ITunesInfo(Element parent) {
24 | this.parent = parent;
25 | this.iTunesNamespace = this.parent.getNamespaceForPrefix("itunes");
26 | }
27 |
28 | public String getAuthor() {
29 | if (this.author != null) {
30 | return this.author;
31 | }
32 |
33 | Element authorElement = this.parent.element(QName.get("author", this.iTunesNamespace));
34 | if (authorElement == null) {
35 | return null;
36 | }
37 |
38 | return this.author = authorElement.getText();
39 | }
40 |
41 | public String getSubtitle() {
42 | if (this.subtitle != null) {
43 | return this.subtitle;
44 | }
45 |
46 | Element subtitleElement = this.parent.element(QName.get("subtitle", this.iTunesNamespace));
47 | if (subtitleElement == null) {
48 | return null;
49 | }
50 |
51 | return this.subtitle = subtitleElement.getText();
52 | }
53 |
54 | public String getSummary() {
55 | if (this.summary != null) {
56 | return this.summary;
57 | }
58 |
59 | Element summaryElement = this.parent.element(QName.get("summary", this.iTunesNamespace));
60 | if (summaryElement == null) {
61 | return null;
62 | }
63 |
64 | return this.summary = summaryElement.getText();
65 | }
66 |
67 | public boolean isBlocked() {
68 | if (this.block != null) {
69 | return this.block;
70 | }
71 |
72 | Element blockElement = this.parent.element(QName.get("block", this.iTunesNamespace));
73 | if (blockElement == null) {
74 | return this.block = false;
75 | }
76 |
77 | return this.block = "yes".equalsIgnoreCase(blockElement.getTextTrim());
78 | }
79 |
80 | public ExplicitLevel getExplicit() {
81 | if (this.explicit != null) {
82 | return this.explicit;
83 | }
84 |
85 | Element explicitElement = this.parent.element(QName.get("explicit", this.iTunesNamespace));
86 | if (explicitElement == null) {
87 | return this.explicit = ExplicitLevel.UNKNOWN;
88 | }
89 |
90 | String explicitText = explicitElement.getTextTrim();
91 | if ("yes".equalsIgnoreCase(explicitText)) {
92 | return this.explicit = ExplicitLevel.EXPLICIT;
93 | }
94 |
95 | if ("no".equalsIgnoreCase(explicitText)) {
96 | return this.explicit = ExplicitLevel.NO;
97 | }
98 |
99 | if ("clean".equalsIgnoreCase(explicitText)) {
100 | return this.explicit = ExplicitLevel.CLEAN;
101 | }
102 |
103 | return this.explicit = ExplicitLevel.UNKNOWN;
104 | }
105 |
106 | public URL getImage() throws MalformedURLException {
107 | String imageString = this.getImageString();
108 |
109 | if (imageString == null) {
110 | return null;
111 | }
112 |
113 | return this.image = new URL(imageString);
114 | }
115 |
116 | public String getImageString() {
117 | if (this.imageString != null) {
118 | return this.imageString;
119 | }
120 |
121 | Element imageElement = this.parent.element(QName.get("image", this.iTunesNamespace));
122 | if (imageElement == null) {
123 | return null;
124 | }
125 |
126 | return this.imageString = imageElement.attributeValue("href");
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/test/java/com/icosillion/podengine/models/EpisodeTest.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import com.icosillion.podengine.exceptions.DateFormatException;
4 | import com.icosillion.podengine.exceptions.InvalidFeedException;
5 | import com.icosillion.podengine.exceptions.MalformedFeedException;
6 | import com.icosillion.podengine.utils.DateUtils;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 |
10 | import java.io.IOException;
11 | import java.net.MalformedURLException;
12 | import java.nio.file.Files;
13 | import java.nio.file.Paths;
14 | import java.util.List;
15 | import java.util.Set;
16 |
17 | import static org.junit.Assert.assertEquals;
18 | import static org.junit.Assert.assertFalse;
19 | import static org.junit.Assert.assertTrue;
20 |
21 | public class EpisodeTest {
22 |
23 | private Podcast podcast;
24 |
25 | @Before
26 | public void setup() throws IOException, InvalidFeedException, MalformedFeedException {
27 | String source = new String(Files.readAllBytes(Paths.get("feed.rss")));
28 | podcast = new Podcast(source);
29 | }
30 |
31 | @Test
32 | public void testEpisode() throws MalformedFeedException, MalformedURLException, DateFormatException {
33 | List episodes = podcast.getEpisodes();
34 | Episode episode = episodes.get(0);
35 |
36 | assertEquals("Episode 1: Are you not getting bored yet?", episode.getTitle());
37 | assertEquals("Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.", episode.getDescription());
38 | assertEquals("https://podcast-feed-library.owl.im/episodes/1", episode.getLink().toString());
39 | assertEquals("Icosillion", episode.getAuthor());
40 | Set categories = episode.getCategories();
41 | assertTrue(categories.contains("Technology"));
42 | assertTrue(categories.contains("Testing"));
43 | assertEquals("https://podcast-feed-library.owl.im/episodes/1/comments", episode.getComments().toString());
44 | assertEquals("https://podcast-feed-library.owl.im/episodes/1", episode.getGUID());
45 | assertEquals(DateUtils.stringToDate("Mon, 28 Nov 2016 13:30:00 GMT"), episode.getPubDate());
46 | assertEquals("Master Feed", episode.getSourceName());
47 | assertEquals("http://podcast-feed-library.owl.im/feed.rss", episode.getSourceURL().toString());
48 | assertEquals("Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.\n" +
49 | " The show notes live in this section, but we have nothing else interesting to say.\n" +
50 | " ", episode.getContentEncoded());
51 |
52 | //Enclosure
53 | Episode.Enclosure enclosure = episode.getEnclosure();
54 | assertEquals("https://podcast-feed-library.owl.im/audio/episode-1.mp3", enclosure.getURL().toString());
55 | assertEquals(1234000L, (long) enclosure.getLength());
56 | assertEquals("audio/mp3", enclosure.getType());
57 |
58 | //iTunes Info
59 | ITunesItemInfo iTunesInfo = episode.getITunesInfo();
60 | assertEquals("Icosillion", iTunesInfo.getAuthor());
61 | assertEquals("Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.", iTunesInfo.getSubtitle());
62 | assertEquals("Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.", iTunesInfo.getSummary());
63 | assertFalse(iTunesInfo.isBlocked());
64 | assertEquals(ITunesInfo.ExplicitLevel.CLEAN, iTunesInfo.getExplicit());
65 | assertEquals("https://podcast-feed-library.owl.im/images/artwork.png", iTunesInfo.getImage().toString());
66 | assertEquals("12:34", iTunesInfo.getDuration());
67 | assertFalse(iTunesInfo.isClosedCaptioned());
68 | assertEquals(1, (int) iTunesInfo.getOrder());
69 | assertEquals(1, (int) iTunesInfo.getSeasonNumber());
70 | assertEquals(1, (int) iTunesInfo.getEpisodeNumber());
71 |
72 | }
73 |
74 | @Test
75 | public void testEpisodeWithExplicitNo()
76 | {
77 | List episodes = podcast.getEpisodes();
78 | Episode episode = episodes.get(1);
79 | ITunesItemInfo iTunesInfo = episode.getITunesInfo();
80 | assertEquals(ITunesInfo.ExplicitLevel.NO, iTunesInfo.getExplicit());
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/ITunesItemInfo.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import org.dom4j.Element;
4 | import org.dom4j.QName;
5 |
6 | public class ITunesItemInfo extends ITunesInfo {
7 |
8 | public enum EpisodeType {
9 | FULL, TRAILER, BONUS, UNKNOWN
10 | }
11 |
12 | private String duration;
13 | private Boolean isClosedCaptioned;
14 | private Integer order;
15 | private Integer seasonNumber;
16 | private Integer episodeNumber;
17 | private String title;
18 | private EpisodeType episodeType;
19 |
20 | public ITunesItemInfo(Element parent) {
21 | super(parent);
22 | }
23 |
24 | public String getDuration() {
25 | if (this.duration != null) {
26 | return this.duration;
27 | }
28 |
29 | Element durationElement = this.parent.element(QName.get("duration", this.iTunesNamespace));
30 | if (durationElement == null) {
31 | return null;
32 | }
33 |
34 | return this.duration = durationElement.getText();
35 | }
36 |
37 | public boolean isClosedCaptioned() {
38 | if (this.isClosedCaptioned != null) {
39 | return this.isClosedCaptioned;
40 | }
41 |
42 | Element isClosedCaptionedElement = this.parent.element(QName.get("isClosedCaptioned", this.iTunesNamespace));
43 | if (isClosedCaptionedElement == null) {
44 | return this.isClosedCaptioned = false;
45 | }
46 |
47 | if ("yes".equalsIgnoreCase(isClosedCaptionedElement.getTextTrim())) {
48 | return this.isClosedCaptioned = true;
49 | }
50 |
51 | return this.isClosedCaptioned = false;
52 | }
53 |
54 | public Integer getOrder() {
55 | if (this.order != null) {
56 | return this.order;
57 | }
58 |
59 | Element orderElement = this.parent.element(QName.get("order", this.iTunesNamespace));
60 | if (orderElement == null) {
61 | return null;
62 | }
63 |
64 | try {
65 | return this.order = Integer.parseInt(orderElement.getTextTrim());
66 | } catch (NumberFormatException e) {
67 | return null;
68 | }
69 | }
70 |
71 | public Integer getEpisodeNumber() {
72 | if (this.episodeNumber != null) {
73 | return this.episodeNumber;
74 | }
75 |
76 | Element episodeNumberElement = this.parent.element(QName.get("episode", this.iTunesNamespace));
77 | if (episodeNumberElement == null) {
78 | return null;
79 | }
80 |
81 | String rawEpisodesNumber = episodeNumberElement.getTextTrim();
82 | try {
83 | return this.episodeNumber = Integer.parseInt(rawEpisodesNumber);
84 | } catch (NumberFormatException e) {
85 | return null;
86 | }
87 | }
88 |
89 | public Integer getSeasonNumber() {
90 | if (this.seasonNumber != null) {
91 | return this.seasonNumber;
92 | }
93 |
94 | Element seasonNumberElement = this.parent.element(QName.get("season", this.iTunesNamespace));
95 | if (seasonNumberElement == null) {
96 | return null;
97 | }
98 |
99 | String rawSeasonNumber = seasonNumberElement.getTextTrim();
100 | try {
101 | return this.seasonNumber = Integer.parseInt(rawSeasonNumber);
102 | } catch (NumberFormatException e) {
103 | return null;
104 | }
105 | }
106 |
107 | public String getTitle() {
108 | if (this.title != null) {
109 | return this.title;
110 | }
111 |
112 | Element titleElement = this.parent.element(QName.get("title", this.iTunesNamespace));
113 | if (titleElement == null) {
114 | return null;
115 | }
116 |
117 | return this.title = titleElement.getText();
118 | }
119 |
120 | public EpisodeType getEpisodeType() {
121 | if (this.episodeType != null) {
122 | return this.episodeType;
123 | }
124 |
125 | Element episodeTypeElement = this.parent.element(QName.get("episodeType", this.iTunesNamespace));
126 | if (episodeTypeElement == null) {
127 | return this.episodeType = EpisodeType.UNKNOWN;
128 | }
129 |
130 | String rawEpisodeType = episodeTypeElement.getTextTrim().toLowerCase();
131 | switch (rawEpisodeType) {
132 | case "bonus":
133 | this.episodeType = EpisodeType.BONUS;
134 | break;
135 | case "trailer":
136 | this.episodeType = EpisodeType.TRAILER;
137 | break;
138 | case "full":
139 | this.episodeType = EpisodeType.FULL;
140 | break;
141 | default:
142 | this.episodeType = EpisodeType.UNKNOWN;
143 | break;
144 | }
145 |
146 | return this.episodeType;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/test/java/com/icosillion/podengine/models/PodcastOverviewTest.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import com.icosillion.podengine.exceptions.DateFormatException;
4 | import com.icosillion.podengine.exceptions.InvalidFeedException;
5 | import com.icosillion.podengine.exceptions.MalformedFeedException;
6 | import com.icosillion.podengine.utils.DateUtils;
7 | import org.junit.Before;
8 | import org.junit.Test;
9 |
10 | import java.io.*;
11 | import java.net.MalformedURLException;
12 | import java.nio.file.Files;
13 | import java.nio.file.Paths;
14 | import java.util.Set;
15 |
16 | import static org.junit.Assert.*;
17 |
18 | public class PodcastOverviewTest {
19 |
20 | private Podcast podcast;
21 |
22 | @Before
23 | public void setup() throws IOException, InvalidFeedException, MalformedFeedException {
24 | String source = new String(Files.readAllBytes(Paths.get("feed.rss")));
25 | podcast = new Podcast(source);
26 | }
27 |
28 | @Test
29 | public void testOverview() throws MalformedFeedException, MalformedURLException, DateFormatException {
30 | assertEquals("Testing Feed", podcast.getTitle());
31 | assertEquals("A dummy podcast feed for testing the Podcast Feed Library.", podcast.getDescription());
32 | assertEquals("https://podcast-feed-library.owl.im/feed", podcast.getLink().toString());
33 | assertEquals("en-GB", podcast.getLanguage());
34 | assertEquals("Copyright © 2017 Icosillion", podcast.getCopyright());
35 | assertEquals("Marcus Lewis (marcus@icosillion.com)", podcast.getManagingEditor());
36 | assertEquals("Marcus Lewis (marcus@icosillion.com)", podcast.getWebMaster());
37 | assertEquals("Mon, 12 Dec 2016 15:30:00 GMT", podcast.getPubDateString());
38 | assertEquals(DateUtils.stringToDate("Mon, 12 Dec 2016 15:30:00 GMT"), podcast.getPubDate());
39 | assertEquals(DateUtils.stringToDate("Mon, 12 Dec 2016 15:30:00 GMT"), podcast.getLastBuildDate());
40 | assertEquals("Mon, 12 Dec 2016 15:30:00 GMT", podcast.getLastBuildDateString());
41 | assertArrayEquals(new String[] { "Technology" }, podcast.getCategories());
42 | assertEquals("Handcrafted", podcast.getGenerator());
43 | assertEquals("https://podcast-feed-library.owl.im/docs", podcast.getDocs().toString());
44 | assertEquals(60, (int) podcast.getTTL());
45 | assertEquals("https://podcast-feed-library.owl.im/images/artwork.png", podcast.getImageURL().toString());
46 | assertNull(podcast.getPICSRating());
47 |
48 | Set skipHours = podcast.getSkipHours();
49 | assertTrue(skipHours.contains(0));
50 | assertTrue(skipHours.contains(4));
51 | assertTrue(skipHours.contains(8));
52 | assertTrue(skipHours.contains(12));
53 | assertTrue(skipHours.contains(16));
54 |
55 | Set skipDays = podcast.getSkipDays();
56 | assertTrue(skipDays.contains("Monday"));
57 | assertTrue(skipDays.contains("Wednesday"));
58 | assertTrue(skipDays.contains("Friday"));
59 | assertArrayEquals(new String[] { "podcast", "java", "xml", "dom4j", "icosillion", "maven" } , podcast.getKeywords());
60 | assertEquals(2, podcast.getEpisodes().size());
61 | }
62 |
63 | @Test
64 | public void testTextInput() throws MalformedURLException {
65 | TextInputInfo textInput = podcast.getTextInput();
66 | assertEquals("Feedback", textInput.getTitle());
67 | assertEquals("Feedback for the Testing Feed", textInput.getDescription());
68 | assertEquals("feedback", textInput.getName());
69 | assertEquals("https://podcast-feed-library.owl.im/feedback/submit", textInput.getLink().toString());
70 | }
71 |
72 | @Test
73 | public void testITunesInfo() throws Exception {
74 | ITunesChannelInfo iTunesInfo = podcast.getITunesInfo();
75 | assertEquals("Icosillion", iTunesInfo.getAuthor());
76 | assertEquals("A dummy podcast feed for testing the Podcast Feed Library.", iTunesInfo.getSubtitle());
77 | assertEquals("This podcast brings testing capabilities to the Podcast Feed Library", iTunesInfo.getSummary());
78 | assertEquals(false, iTunesInfo.isBlocked());
79 | assertEquals(ITunesInfo.ExplicitLevel.CLEAN, iTunesInfo.getExplicit());
80 | assertEquals("https://podcast-feed-library.owl.im/images/artwork.png", iTunesInfo.getImage().toString());
81 | assertEquals(ITunesChannelInfo.FeedType.SERIAL, iTunesInfo.getType());
82 | }
83 |
84 | @Test
85 | public void testITunesOwnerInfo() {
86 | ITunesOwner iTunesOwner = podcast.getITunesInfo().getOwner();
87 | assertEquals("Icosillion", iTunesOwner.getName());
88 | assertEquals("hello@icosillion.com", iTunesOwner.getEmail());
89 | }
90 |
91 | @Test
92 | public void testCloudInfo() {
93 | CloudInfo cloudInfo = podcast.getCloud();
94 | assertEquals("rpc.owl.im", cloudInfo.getDomain());
95 | assertEquals(8080, (int) cloudInfo.getPort());
96 | assertEquals("/rpc", cloudInfo.getPath());
97 | assertEquals("owl.register", cloudInfo.getRegisterProcedure());
98 | assertEquals("xml-rpc", cloudInfo.getProtocol());
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/feed.rss:
--------------------------------------------------------------------------------
1 |
2 |
3 | Testing Feed
4 | https://podcast-feed-library.owl.im/feed
5 | Mon, 12 Dec 2016 15:30:00 GMT
6 | Mon, 12 Dec 2016 15:30:00 GMT
7 | A dummy podcast feed for testing the Podcast Feed Library.
8 | Marcus Lewis (marcus@icosillion.com)
9 | Marcus Lewis (marcus@icosillion.com)
10 | en-GB
11 | Copyright © 2017 Icosillion
12 | Handcrafted
13 | https://podcast-feed-library.owl.im/docs
14 | 60
15 |
16 | 0
17 | 4
18 | 8
19 | 12
20 | 16
21 |
22 |
23 | Monday
24 | Wednesday
25 | Friday
26 |
27 |
28 | Feedback
29 | Feedback for the Testing Feed
30 | feedback
31 | https://podcast-feed-library.owl.im/feedback/submit
32 |
33 |
34 | Icosillion
35 | A dummy podcast feed for testing the Podcast Feed Library.
36 | This podcast brings testing capabilities to the Podcast Feed Library
37 | podcast, java, xml, dom4j, icosillion, maven
38 | clean
39 |
40 |
41 | Icosillion
42 | hello@icosillion.com
43 |
44 | no
45 |
46 | serial
47 | -
48 | Episode 1: Are you not getting bored yet?
49 | Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.
50 | Mon, 28 Nov 2016 13:30:00 GMT
51 |
52 | https://podcast-feed-library.owl.im/episodes/1
53 | https://podcast-feed-library.owl.im/episodes/1
54 | https://podcast-feed-library.owl.im/episodes/1/comments
55 | Technology
56 | Testing
57 | Master Feed
58 | Icosillion
59 | Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.
60 | Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.
61 | clean
62 | 12:34
63 | no
64 |
65 | no
66 | 1
67 | 1
68 | 1
69 | 1
70 | Our hosts start getting bored of running a testing podcast feed. There's probably some moaning about Apple too. This is a technology podcast after all.
71 | The show notes live in this section, but we have nothing else interesting to say.
72 |
73 |
74 | -
75 | Episode 2: Most podcasts have more than one episode
76 | Our hosts realize they need a second episode.
77 | Tue, 28 Oct 2019 13:30:00 GMT
78 |
79 | https://podcast-feed-library.owl.im/episodes/2
80 | https://podcast-feed-library.owl.im/episodes/2
81 | https://podcast-feed-library.owl.im/episodes/2/comments
82 | Technology
83 | Testing
84 | Master Feed
85 | Icosillion
86 | Our hosts realize they need a second episode.
87 | Our hosts realize they need a second episode.
88 | no
89 | 12:34
90 | no
91 |
92 | no
93 | 2
94 | 1
95 | 2
96 | Our hosts realize they need a second episode.
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/Episode.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import com.icosillion.podengine.exceptions.DateFormatException;
4 | import com.icosillion.podengine.exceptions.MalformedFeedException;
5 | import com.icosillion.podengine.utils.DateUtils;
6 | import org.dom4j.Attribute;
7 | import org.dom4j.Element;
8 | import org.dom4j.Namespace;
9 | import org.dom4j.QName;
10 |
11 | import java.net.MalformedURLException;
12 | import java.net.URL;
13 | import java.util.*;
14 |
15 | public class Episode {
16 |
17 | public static class Enclosure {
18 |
19 | private URL url;
20 | private Long length;
21 | private String mimeType;
22 |
23 | private final Element enclosureElement;
24 |
25 | public Enclosure(Element enclosureElement) {
26 | this.enclosureElement = enclosureElement;
27 | }
28 |
29 | public URL getURL() throws MalformedFeedException, MalformedURLException {
30 | if (this.url != null) {
31 | return this.url;
32 | }
33 |
34 | Attribute urlAttribute = this.enclosureElement.attribute("url");
35 | if (urlAttribute == null) {
36 | throw new MalformedFeedException("Missing required URL attribute for element Enclosure.");
37 | }
38 |
39 | return this.url = new URL(urlAttribute.getValue());
40 | }
41 |
42 | public Long getLength() throws MalformedFeedException {
43 | if (this.length != null) {
44 | return this.length;
45 | }
46 |
47 | Attribute lengthAttribute = this.enclosureElement.attribute("length");
48 | if (lengthAttribute == null) {
49 | throw new MalformedFeedException("Missing required Length attribute for element Enclosure.");
50 | }
51 |
52 | try {
53 | return this.length = Long.parseLong(lengthAttribute.getValue());
54 | } catch (NumberFormatException e) {
55 | throw new MalformedFeedException("Invalid length specified for element Enclosure.");
56 | }
57 | }
58 |
59 | public String getType() throws MalformedFeedException {
60 | if (this.mimeType != null) {
61 | return this.mimeType;
62 | }
63 |
64 | Attribute typeAttribute = this.enclosureElement.attribute("type");
65 | if (typeAttribute == null) {
66 | throw new MalformedFeedException("Missing required Type attribute for element Enclosure.");
67 | }
68 |
69 | return this.mimeType = typeAttribute.getValue();
70 | }
71 | }
72 |
73 | private String title;
74 | private URL link;
75 | private String description;
76 | private String author;
77 | private Set categories;
78 | private URL comments;
79 | private Enclosure enclosure;
80 | private String guid;
81 | private Date pubDate;
82 | private String sourceName;
83 | private URL sourceLink;
84 | private ITunesItemInfo iTunesItemInfo;
85 | private String contentEncoded;
86 |
87 | private final Element itemElement;
88 |
89 | public Episode(Element itemElement) {
90 | this.itemElement = itemElement;
91 | }
92 |
93 | //Required Tags
94 | public String getTitle() throws MalformedFeedException {
95 | if (this.title != null) {
96 | return this.title;
97 | }
98 |
99 | Element titleElement = this.itemElement.element("title");
100 | if (titleElement == null) {
101 | throw new MalformedFeedException("Item is missing required element title.");
102 | }
103 |
104 | return this.title = titleElement.getText();
105 | }
106 |
107 | public String getDescription() throws MalformedFeedException {
108 | if (this.description != null) {
109 | return this.description;
110 | }
111 |
112 | Element descriptionElement = this.itemElement.element("description");
113 | if (descriptionElement == null) {
114 | throw new MalformedFeedException("Item is missing required element description.");
115 | }
116 |
117 | return this.description = descriptionElement.getText();
118 | }
119 |
120 | //Optional Tags
121 | public URL getLink() throws MalformedURLException {
122 | if (this.link != null) {
123 | return this.link;
124 | }
125 |
126 | Element linkElement = this.itemElement.element("link");
127 | if (linkElement == null) {
128 | return null;
129 | }
130 |
131 | if ("atom".equalsIgnoreCase(linkElement.getNamespacePrefix())) {
132 | return this.link = new URL(linkElement.attributeValue("href"));
133 | }
134 |
135 | return this.link = new URL(linkElement.getText());
136 | }
137 |
138 | public Enclosure getEnclosure() {
139 | if (this.enclosure != null) {
140 | return this.enclosure;
141 | }
142 |
143 | Element enclosureElement = this.itemElement.element("enclosure");
144 | if (enclosureElement == null) {
145 | return null;
146 | }
147 |
148 | return this.enclosure = new Enclosure(enclosureElement);
149 | }
150 |
151 | public String getAuthor() {
152 | if (this.author != null) {
153 | return this.author;
154 | }
155 |
156 | Element authorElement = this.itemElement.element("author");
157 | if (authorElement == null) {
158 | return null;
159 | }
160 |
161 | return this.author = authorElement.getText();
162 | }
163 |
164 | public Set getCategories() {
165 | if (this.categories != null) {
166 | return this.categories;
167 | }
168 |
169 | List categoryElements = this.itemElement.elements("category");
170 |
171 | Set categories = new HashSet<>();
172 | for (Element element : categoryElements) {
173 | categories.add(element.getTextTrim());
174 | }
175 |
176 | return this.categories = Collections.unmodifiableSet(categories);
177 | }
178 |
179 | public URL getComments() throws MalformedURLException {
180 | if (this.comments != null) {
181 | return this.comments;
182 | }
183 |
184 | Element commentsElement = this.itemElement.element("comments");
185 | if (commentsElement == null) {
186 | return null;
187 | }
188 |
189 | return this.comments = new URL(commentsElement.getTextTrim());
190 | }
191 |
192 | public String getGUID() {
193 | if (this.guid != null) {
194 | return this.guid;
195 | }
196 |
197 | Element guidElement = this.itemElement.element("guid");
198 | if (guidElement == null) {
199 | return null;
200 | }
201 |
202 | return this.guid = guidElement.getTextTrim();
203 | }
204 |
205 | public Date getPubDate() throws DateFormatException {
206 | if (this.pubDate != null) {
207 | return this.pubDate;
208 | }
209 |
210 | Element pubDateElement = this.itemElement.element("pubDate");
211 | if (pubDateElement == null) {
212 | return null;
213 | }
214 |
215 | return this.pubDate = DateUtils.stringToDate(pubDateElement.getTextTrim());
216 | }
217 |
218 | public String getSourceName() {
219 | if (this.sourceName != null) {
220 | return this.sourceName;
221 | }
222 |
223 | Element sourceElement = this.itemElement.element("source");
224 | if (sourceElement == null) {
225 | return null;
226 | }
227 |
228 | return this.sourceName = sourceElement.getText();
229 | }
230 |
231 | public URL getSourceURL() throws MalformedFeedException, MalformedURLException {
232 | if (this.sourceLink != null) {
233 | return this.sourceLink;
234 | }
235 |
236 | Element sourceElement = this.itemElement.element("source");
237 | if (sourceElement == null) {
238 | return null;
239 | }
240 |
241 | Attribute urlAttribute = sourceElement.attribute("url");
242 | if (urlAttribute == null) {
243 | throw new MalformedFeedException("Missing required attribute URL for element Source.");
244 | }
245 |
246 | return this.sourceLink = new URL(urlAttribute.getText());
247 | }
248 |
249 | public ITunesItemInfo getITunesInfo() {
250 | if (this.iTunesItemInfo != null) {
251 | return this.iTunesItemInfo;
252 | }
253 |
254 | return this.iTunesItemInfo = new ITunesItemInfo(this.itemElement);
255 | }
256 |
257 | public String getContentEncoded() {
258 | if (this.contentEncoded != null) {
259 | return this.contentEncoded;
260 | }
261 |
262 | Namespace namespace = this.itemElement.getNamespaceForPrefix("content");
263 | Element contentEncodedElement = this.itemElement.element(QName.get("encoded", namespace));
264 | if (contentEncodedElement == null) {
265 | return null;
266 | }
267 |
268 | return this.contentEncoded = contentEncodedElement.getText();
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/main/java/com/icosillion/podengine/models/Podcast.java:
--------------------------------------------------------------------------------
1 | package com.icosillion.podengine.models;
2 |
3 | import com.icosillion.podengine.exceptions.DateFormatException;
4 | import com.icosillion.podengine.exceptions.InvalidFeedException;
5 | import com.icosillion.podengine.exceptions.MalformedFeedException;
6 | import com.icosillion.podengine.utils.DateUtils;
7 | import org.apache.commons.io.IOUtils;
8 | import org.apache.commons.io.input.BOMInputStream;
9 | import org.dom4j.Document;
10 | import org.dom4j.DocumentException;
11 | import org.dom4j.DocumentHelper;
12 | import org.dom4j.Element;
13 | import java.io.IOException;
14 | import java.io.InputStream;
15 | import java.net.HttpURLConnection;
16 | import java.net.MalformedURLException;
17 | import java.net.URL;
18 | import java.util.*;
19 |
20 | public class Podcast {
21 |
22 | private String xmlData;
23 | private Document document;
24 | private URL feedURL;
25 | private URL resolvedURL;
26 |
27 | private Element rootElement, channelElement;
28 |
29 | //Caching
30 | private String title, description, language, copyright, managingEditor, webMaster, pubDateString,
31 | lastBuildDateString, generator, picsRating, docsString;
32 | private URL link, docs;
33 | private Date pubDate, lastBuildDate;
34 | private CloudInfo cloudInfo;
35 | private Integer ttl;
36 | private TextInputInfo textInputInfo;
37 | private Set skipHours;
38 | private Set skipDays;
39 | private ITunesChannelInfo iTunesChannelInfo;
40 | private List episodes;
41 |
42 | public Podcast(URL feed) throws InvalidFeedException, MalformedFeedException {
43 | HttpURLConnection ic = null;
44 | InputStream is = null;
45 | BOMInputStream bomInputStream = null;
46 |
47 | try {
48 | //Open Connection
49 | ic = (HttpURLConnection) feed.openConnection();
50 | ic.setInstanceFollowRedirects(true);
51 | ic.setRequestProperty("User-Agent", "PodEngine/2.2");
52 | is = ic.getInputStream();
53 |
54 | //Create BOMInputStream to strip any Byte Order Marks
55 | bomInputStream = new BOMInputStream(is, false);
56 |
57 | this.feedURL = feed;
58 | this.resolvedURL = ic.getURL();
59 | this.xmlData = IOUtils.toString(bomInputStream);
60 | this.document = DocumentHelper.parseText(xmlData);
61 | this.rootElement = this.document.getRootElement();
62 | this.channelElement = this.rootElement.element("channel");
63 | if (this.channelElement == null) {
64 | throw new MalformedFeedException("Missing required channel element.");
65 | }
66 | } catch (IOException e) {
67 | throw new InvalidFeedException("Error reading feed.", e);
68 | } catch (DocumentException e) {
69 | throw new InvalidFeedException("Error parsing feed XML.", e);
70 | } finally {
71 | IOUtils.closeQuietly(bomInputStream);
72 | IOUtils.closeQuietly(is);
73 | }
74 | }
75 |
76 | public Podcast(String xml) throws MalformedFeedException {
77 | try {
78 | this.xmlData = xml;
79 | this.document = DocumentHelper.parseText(this.xmlData);
80 | this.rootElement = this.document.getRootElement();
81 | this.channelElement = this.rootElement.element("channel");
82 | if (this.channelElement == null) {
83 | throw new MalformedFeedException("Missing required element 'channel'.");
84 | }
85 | } catch (DocumentException e) {
86 | throw new MalformedFeedException("Error parsing feed.", e);
87 | }
88 | }
89 |
90 | public Podcast(String xml, URL feed) throws MalformedFeedException {
91 | try {
92 | this.xmlData = xml;
93 | this.feedURL = feed;
94 | this.document = DocumentHelper.parseText(this.xmlData);
95 | this.rootElement = this.document.getRootElement();
96 | this.channelElement = this.rootElement.element("channel");
97 | if (this.channelElement == null) {
98 | throw new MalformedFeedException("Missing required element 'channel'.");
99 | }
100 | } catch (DocumentException e) {
101 | throw new MalformedFeedException("Error parsing document.", e);
102 | }
103 | }
104 |
105 | public String getTitle() throws MalformedFeedException {
106 | if (this.title != null)
107 | return this.title;
108 |
109 | Element titleElement = this.channelElement.element("title");
110 | if (titleElement == null) {
111 | throw new MalformedFeedException("Missing required title element.");
112 | }
113 |
114 | return this.title = titleElement.getText();
115 | }
116 |
117 | public String getDescription() throws MalformedFeedException {
118 | if (this.description != null)
119 | return this.description;
120 |
121 | Element descriptionElement = this.channelElement.element("description");
122 | if (descriptionElement == null) {
123 | throw new MalformedFeedException("Missing required description element.");
124 | }
125 |
126 | return this.description = descriptionElement.getText();
127 | }
128 |
129 | public URL getLink() throws MalformedURLException, MalformedFeedException {
130 | if (this.link != null) {
131 | return this.link;
132 | }
133 |
134 | Element linkElement = this.channelElement.element("link");
135 | if (linkElement == null)
136 | throw new MalformedFeedException("Missing required link element.");
137 |
138 | if ("atom".equalsIgnoreCase(linkElement.getNamespacePrefix())) {
139 | return new URL(linkElement.attributeValue("href"));
140 | }
141 |
142 | //TODO Handle URL Exceptions?
143 |
144 | return this.link = new URL(linkElement.getText());
145 | }
146 |
147 | //Optional Params (Can return null)
148 |
149 | public String getLanguage() {
150 | if (this.language != null)
151 | return this.language;
152 |
153 | Element languageElement = this.channelElement.element("language");
154 | if (languageElement == null) {
155 | return null;
156 | }
157 |
158 | return this.language = languageElement.getText();
159 | }
160 |
161 | public String getCopyright() {
162 | if (this.copyright != null) {
163 | return this.copyright;
164 | }
165 |
166 | Element copyrightElement = this.channelElement.element("copyright");
167 | if (copyrightElement == null) {
168 | return null;
169 | }
170 |
171 | return this.copyright = copyrightElement.getText();
172 | }
173 |
174 | public String getManagingEditor() {
175 | if (this.managingEditor != null) {
176 | return this.managingEditor;
177 | }
178 |
179 | Element managingEditorElement = this.channelElement.element("managingEditor");
180 | if (managingEditorElement == null)
181 | return null;
182 |
183 | return this.managingEditor = managingEditorElement.getText();
184 | }
185 |
186 | public String getWebMaster() {
187 | if (this.webMaster != null) {
188 | return this.webMaster;
189 | }
190 |
191 | Element webMasterElement = this.channelElement.element("webMaster");
192 | if (webMasterElement == null) {
193 | return null;
194 | }
195 |
196 | return this.webMaster = webMasterElement.getText();
197 | }
198 |
199 | public Date getPubDate() throws DateFormatException {
200 | if (this.pubDate != null) {
201 | return this.pubDate;
202 | }
203 |
204 | String pubDateString = getPubDateString();
205 | if (pubDateString == null) {
206 | return null;
207 | }
208 |
209 | return this.pubDate = DateUtils.stringToDate(pubDateString.trim());
210 | }
211 |
212 | public String getPubDateString() {
213 | if (this.pubDateString != null) {
214 | return this.pubDateString;
215 | }
216 |
217 | Element pubDateElement = this.channelElement.element("pubDate");
218 | if (pubDateElement == null) {
219 | return null;
220 | }
221 |
222 | return this.pubDateString = pubDateElement.getText();
223 | }
224 |
225 | public Date getLastBuildDate() throws DateFormatException {
226 | if (this.lastBuildDate != null) {
227 | return this.lastBuildDate;
228 | }
229 |
230 | String lastBuildDateString = getLastBuildDateString();
231 | if (lastBuildDateString == null) {
232 | return null;
233 | }
234 |
235 | return this.lastBuildDate = DateUtils.stringToDate(lastBuildDateString);
236 | }
237 |
238 | public String getLastBuildDateString() {
239 | if (this.lastBuildDateString != null) {
240 | return this.lastBuildDateString;
241 | }
242 |
243 | Element lastBuildDateElement = this.channelElement.element("lastBuildDate");
244 | if (lastBuildDateElement == null) {
245 | return null;
246 | }
247 |
248 | return this.lastBuildDateString = lastBuildDateElement.getText();
249 | }
250 |
251 | //TODO Update this with caching
252 | public String[] getCategories() {
253 | List categories = new ArrayList<>();
254 | Element rootElement = this.document.getRootElement();
255 | Element channel = rootElement.element("channel");
256 | boolean hasiTunes = false;
257 | if (channel.element("category") != null) {
258 | for (Element child : (List) channel.elements("category")) {
259 | if (!"itunes".equalsIgnoreCase(child.getNamespacePrefix()) && !hasiTunes) {
260 | categories.add(child.getText());
261 | } else if ("itunes".equalsIgnoreCase(child.getNamespacePrefix()) && !hasiTunes) {
262 | hasiTunes = true;
263 | //Clear Categories
264 | categories.clear();
265 | if (child.elements("category").size() == 0) {
266 | if (child.attribute("text") != null) {
267 | categories.add(child.attributeValue("text"));
268 | } else {
269 | categories.add(child.getText());
270 | }
271 | } else {
272 | String finalCategory;
273 | if (child.attribute("text") != null) {
274 | finalCategory = child.attributeValue("text");
275 | } else {
276 | finalCategory = child.getText();
277 | }
278 |
279 | for (Element category : (List) child.elements("category")) {
280 | if (category.attribute("text") != null) {
281 | finalCategory += " > " + category.attributeValue("text");
282 | } else {
283 | finalCategory += " > " + category.getText();
284 | }
285 | }
286 | categories.add(finalCategory);
287 | }
288 |
289 | } else if (hasiTunes && "itunes".equalsIgnoreCase(child.getNamespacePrefix())) {
290 | if (child.elements("category").size() == 0) {
291 | if (child.attribute("text") != null) {
292 | categories.add(child.attributeValue("text"));
293 | } else {
294 | categories.add(child.getText());
295 | }
296 | } else {
297 | String finalCategory;
298 | if (child.attribute("text") != null) {
299 | finalCategory = child.attributeValue("text");
300 | } else {
301 | finalCategory = child.getText();
302 | }
303 |
304 | for (Element category : (List) child.elements("category")) {
305 | if (category.attribute("text") != null) {
306 | finalCategory += " > " + category.attributeValue("text");
307 | } else {
308 | finalCategory += " > " + category.getText();
309 | }
310 | }
311 | categories.add(finalCategory);
312 | }
313 | }
314 | }
315 | }
316 |
317 | if (categories.size() == 0) {
318 | return new String[0];
319 | }
320 |
321 | String[] output = new String[categories.size()];
322 | categories.toArray(output);
323 | return output;
324 | }
325 |
326 | public String getGenerator() {
327 | if (this.generator != null) {
328 | return this.generator;
329 | }
330 |
331 | Element generatorElement = this.channelElement.element("generator");
332 | if (generatorElement == null) {
333 | return null;
334 | }
335 |
336 | return this.generator = generatorElement.getText();
337 | }
338 |
339 | public URL getDocs() throws MalformedURLException {
340 | if (this.docs != null) {
341 | return this.docs;
342 | }
343 |
344 | String docsString = this.getDocsString();
345 | if (docsString == null) {
346 | return null;
347 | }
348 |
349 | return this.docs = new URL(docsString);
350 | }
351 |
352 | public String getDocsString() {
353 | if (this.docsString != null) {
354 | return this.docsString;
355 | }
356 |
357 | Element docsElement = this.channelElement.element("docs");
358 | if (docsElement == null) {
359 | return null;
360 | }
361 |
362 | return this.docsString = docsElement.getText();
363 | }
364 |
365 | public CloudInfo getCloud() {
366 | if (this.cloudInfo != null) {
367 | return this.cloudInfo;
368 | }
369 |
370 | Element cloudElement = this.channelElement.element("cloud");
371 | if (cloudElement == null) {
372 | return null;
373 | }
374 |
375 | return this.cloudInfo = new CloudInfo(cloudElement);
376 | }
377 |
378 | public Integer getTTL() {
379 | if (this.ttl != null) {
380 | return this.ttl;
381 | }
382 |
383 | Element ttlElement = this.channelElement.element("ttl");
384 | if (ttlElement == null) {
385 | return null;
386 | }
387 |
388 | try {
389 | return this.ttl = Integer.valueOf(ttlElement.getTextTrim());
390 | } catch (NumberFormatException e) {
391 | return null;
392 | }
393 | }
394 |
395 | public URL getImageURL() throws MalformedURLException {
396 | Element thumbnailElement = this.channelElement.element("thumbnail");
397 | if (thumbnailElement != null)
398 | return new URL(thumbnailElement.attributeValue("url"));
399 | for (Element image : (List) this.channelElement.elements("image")) {
400 | if ("itunes".equalsIgnoreCase(image.getNamespacePrefix())) {
401 | return new URL(image.attributeValue("href"));
402 | } else if (image.element("url") != null) {
403 | return new URL(image.element("url").getText());
404 | }
405 | }
406 |
407 | return null;
408 | }
409 |
410 | public String getPICSRating() {
411 | if (this.picsRating != null) {
412 | return this.picsRating;
413 | }
414 |
415 | Element ratingElement = this.channelElement.element("rating");
416 | if (ratingElement == null) {
417 | return null;
418 | }
419 |
420 | return this.picsRating = ratingElement.getText();
421 | }
422 |
423 | public TextInputInfo getTextInput() {
424 | if (this.textInputInfo != null)
425 | return this.textInputInfo;
426 |
427 | Element textInputElement = this.channelElement.element("textInput");
428 | if (textInputElement == null) {
429 | return null;
430 | }
431 |
432 | return this.textInputInfo = new TextInputInfo(textInputElement);
433 | }
434 |
435 | public Set getSkipHours() throws MalformedFeedException {
436 | if (this.skipHours != null) {
437 | return this.skipHours;
438 | }
439 |
440 | Element skipHoursElement = this.channelElement.element("skipHours");
441 | if (skipHoursElement == null) {
442 | return null;
443 | }
444 |
445 | List hourElements = skipHoursElement.elements("hour");
446 | if (hourElements.size() == 0) {
447 | return null;
448 | }
449 |
450 | Set skipHours = new HashSet<>();
451 |
452 | for (Object hourObject : hourElements) {
453 | if (hourObject instanceof Element) {
454 | Element hourElement = (Element) hourObject;
455 | int hour;
456 | try {
457 | hour = Integer.valueOf(hourElement.getTextTrim());
458 | } catch (NumberFormatException e) {
459 | throw new MalformedFeedException("Invalid hour in skipHours element.");
460 | }
461 |
462 | if (hour < 0 || hour > 23) {
463 | throw new MalformedFeedException("Hour in skipHours element is outside of valid range 0 - 23");
464 | }
465 |
466 | skipHours.add(hour);
467 | }
468 | }
469 |
470 | if (skipHours.size() == 0) {
471 | return null;
472 | }
473 |
474 | return this.skipHours = Collections.unmodifiableSet(skipHours);
475 | }
476 |
477 | public Set getSkipDays() throws MalformedFeedException {
478 | if (this.skipDays != null) {
479 | return this.skipDays;
480 | }
481 |
482 | Element skipDaysElement = this.channelElement.element("skipDays");
483 | if (skipDaysElement == null) {
484 | return null;
485 | }
486 |
487 | List dayElements = skipDaysElement.elements("day");
488 | if (dayElements.size() == 0) {
489 | return null;
490 | }
491 |
492 | if (dayElements.size() > 7) {
493 | throw new MalformedFeedException("More than 7 day elements present within skipDays element.");
494 | }
495 |
496 | Set skipDays = new HashSet<>();
497 |
498 | final String[] validDays = new String[]{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday",
499 | "Sunday"};
500 |
501 | for (Object dayObject : dayElements) {
502 | if (dayObject instanceof Element) {
503 | Element dayElement = (Element) dayObject;
504 | String day = dayElement.getTextTrim();
505 | if (day == null || day.isEmpty())
506 | continue;
507 |
508 | boolean valid = false;
509 | for (String validDay : validDays) {
510 | if (day.equalsIgnoreCase(validDay))
511 | valid = true;
512 | }
513 |
514 | if (valid)
515 | skipDays.add(day);
516 | }
517 | }
518 |
519 | if (skipDays.size() == 0) {
520 | return null;
521 | }
522 |
523 | return this.skipDays = Collections.unmodifiableSet(skipDays);
524 | }
525 |
526 | //TODO Update this with caching and convert to Set
527 | public String[] getKeywords() {
528 | List keywords = new ArrayList<>();
529 | Element rootElement = this.document.getRootElement();
530 | Element channel = rootElement.element("channel");
531 | boolean hasiTunes = false;
532 | if (channel.element("keywords") != null) {
533 | for (Element child : (List) channel.elements("keywords")) {
534 | if (!"itunes".equalsIgnoreCase(child.getNamespacePrefix()) && !hasiTunes) {
535 | for (String kw : child.getText().split(",")) {
536 | keywords.add(kw.trim());
537 | }
538 | } else if ("itunes".equalsIgnoreCase(child.getNamespacePrefix()) && !hasiTunes) {
539 | hasiTunes = true;
540 | //Clear Categories
541 | keywords.clear();
542 | for (String kw : child.getText().split(",")) {
543 | keywords.add(kw.trim());
544 | }
545 | } else if (hasiTunes) {
546 | for (String kw : child.getText().split(",")) {
547 | keywords.add(kw.trim());
548 | }
549 | }
550 | }
551 | }
552 |
553 | if (keywords.size() == 0) {
554 | return new String[0];
555 | }
556 |
557 | String[] output = new String[keywords.size()];
558 | keywords.toArray(output);
559 | return output;
560 | }
561 |
562 | //Episodes
563 | public List getEpisodes() {
564 | if (this.episodes != null) {
565 | return this.episodes;
566 | }
567 |
568 | List episodes = new ArrayList<>();
569 | for (Object itemObject : this.channelElement.elements("item")) {
570 | if (!(itemObject instanceof Element)) {
571 | continue;
572 | }
573 |
574 | episodes.add(new Episode((Element) itemObject));
575 | }
576 |
577 | if (episodes.size() == 0) {
578 | return null;
579 | }
580 |
581 | return this.episodes = Collections.unmodifiableList(episodes);
582 | }
583 |
584 | public ITunesChannelInfo getITunesInfo() {
585 | if (this.iTunesChannelInfo != null) {
586 | return this.iTunesChannelInfo;
587 | }
588 |
589 | return this.iTunesChannelInfo = new ITunesChannelInfo(this.channelElement);
590 | }
591 |
592 | public String getXMLData() {
593 | return this.xmlData;
594 | }
595 |
596 | public URL getFeedURL() {
597 | return feedURL;
598 | }
599 |
600 | public URL getResolvedURL() {
601 | return resolvedURL;
602 | }
603 | }
604 |
--------------------------------------------------------------------------------