├── 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 | --------------------------------------------------------------------------------