diff --git a/build.gradle b/build.gradle
index d6f0ae0..d2f2afe 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,7 @@ plugins {
id 'eclipse'
id 'maven-publish'
id 'net.nemerosa.versioning' version '1.3.0'
- id 'com.jfrog.bintray' version '1.1'
+ id 'com.jfrog.bintray' version '1.2'
id 'com.jfrog.artifactory' version '3.0.1'
}
diff --git a/src/main/java/org/schedulesdirect/api/Airing.java b/src/main/java/org/schedulesdirect/api/Airing.java
index d6c6e41..11eee16 100644
--- a/src/main/java/org/schedulesdirect/api/Airing.java
+++ b/src/main/java/org/schedulesdirect/api/Airing.java
@@ -72,7 +72,17 @@ static public enum DolbyStatus {
/**
* An unknown value was provided for Dolby status; report the unknown value as a bug ticket for future inclusion
*/
- UNKNOWN
+ UNKNOWN;
+
+ public static DolbyStatus fromValue(String val) {
+ switch(val.toLowerCase().replaceAll("\\W", "")) {
+ case "dd": return DD;
+ case "dd51": return DD51;
+ case "dolby": return DOLBY;
+ }
+
+ return UNKNOWN;
+ }
}
/**
@@ -251,14 +261,10 @@ static public enum ContentType {
case "stereo": stereo = true; break;
case "dvs": descriptiveVideo = true; break;
case "subtitled": subtitled = true; break;
+ case "SAP": sap = true; break;
default:
if(val.startsWith("D")) { // This is a Dolby marker
- try {
- dolbyStatus = DolbyStatus.valueOf(val.replaceAll(" ", "").replaceAll("\\.", ""));
- } catch(IllegalArgumentException e) {
- LOG.warn(String.format("Unknown DolbyStatus encountered! [%s]", val));
- dolbyStatus = DolbyStatus.UNKNOWN;
- }
+ dolbyStatus = DolbyStatus.fromValue(val);
} else
LOG.warn(String.format("Unknown audio property encountered! [%s]", val));
}
@@ -267,9 +273,6 @@ static public enum ContentType {
timeApproximate = src.optBoolean("timeApproximate");
if(subtitled && src.has("subtitledLanguage"))
subtitleLanguage = src.getString("subtitledLanguage");
- sap = src.optBoolean("sap");
- if(sap && src.has("sapLanguage"))
- sapLanguage = src.getString("sapLanguage");
cableInTheClassroom = src.optBoolean("cableInTheClassroom");
subjectToBlackout = src.optBoolean("subjectToBlackout");
educational = src.optBoolean("educational");
@@ -703,7 +706,7 @@ public void setFinaleStatus(FinaleStatus finaleStatus) {
}
/**
- * @param tvRating the tvRating to set
+ * @param tvRatings the tvRatings to set
*/
public void setTvRatings(ContentRating[] tvRatings) {
this.tvRatings = tvRatings;
diff --git a/src/main/java/org/schedulesdirect/api/Artwork.java b/src/main/java/org/schedulesdirect/api/Artwork.java
new file mode 100644
index 0000000..eba4120
--- /dev/null
+++ b/src/main/java/org/schedulesdirect/api/Artwork.java
@@ -0,0 +1,127 @@
+package org.schedulesdirect.api;
+
+import org.json.JSONObject;
+
+public class Artwork {
+
+ public static enum Size {
+ MASSIVE,
+ LARGE,
+ MEDIUM,
+ SMALL,
+ EXTRA_SMALL,
+ UNKNOWN;
+
+ public static Size fromString(String val) {
+ if(val != null) {
+ switch(val) {
+ case "Ms":
+ return Size.MASSIVE;
+ case "Lg":
+ return Size.LARGE;
+ case "Md":
+ return Size.MEDIUM;
+ case "Sm":
+ return Size.SMALL;
+ case "Xs":
+ return Size.EXTRA_SMALL;
+ }
+ }
+
+ return Size.UNKNOWN;
+ }
+ }
+
+ private String aspect;
+ private int width;
+ private int height;
+ private boolean text;
+ private String category;
+ private String uri;
+ private String tier;
+ private Size size;
+
+ public Artwork(JSONObject obj, EpgClient clnt) {
+ aspect = obj.optString("aspect");
+ String width = obj.optString("width");
+ this.width = width.length() == 0 ? 0 : Integer.parseInt(width);
+ String height = obj.optString("height");
+ this.height = height.length() == 0 ? 0 : Integer.parseInt(height);
+ text = "yes".equalsIgnoreCase(obj.optString("text"));
+ category = obj.optString("category");
+ tier = obj.optString("tier");
+ String uri = obj.optString("uri");
+ if(uri.matches("^https?:\\/\\/.*")) {
+ this.uri = uri;
+ }
+ else {
+ this.uri = String.format("%s/%s/image/%s", clnt.getBaseUrl(), EpgClient.API_VERSION, uri);
+ }
+
+ size = Size.fromString(obj.optString("size"));
+ }
+
+ public String getAspect() {
+ return aspect;
+ }
+
+ public void setAspect(String aspect) {
+ this.aspect = aspect;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public boolean isText() {
+ return text;
+ }
+
+ public void setText(boolean text) {
+ this.text = text;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ public String getTier() {
+ return tier;
+ }
+
+ public void setTier(String tier) {
+ this.tier = tier;
+ }
+
+ public Size getSize() {
+ return size;
+ }
+
+ public void setSize(Size size) {
+ this.size = size;
+ }
+}
diff --git a/src/main/java/org/schedulesdirect/api/EpgClient.java b/src/main/java/org/schedulesdirect/api/EpgClient.java
index fe21bda..576f383 100644
--- a/src/main/java/org/schedulesdirect/api/EpgClient.java
+++ b/src/main/java/org/schedulesdirect/api/EpgClient.java
@@ -51,7 +51,7 @@ static public String getUriPathForLineupId(String id) {
/**
* Constructor
* @param userAgent The user agent to pass along to all SD HTTP requests
- * @param baseUri The base URI to use when constructing URIs/URLs based on relative data in the raw JSON
+ * @param baseUrl The base URI to use when constructing URIs/URLs based on relative data in the raw JSON
*/
public EpgClient(final String userAgent, final String baseUrl) {
this.userAgent = userAgent;
@@ -64,7 +64,6 @@ public EpgClient(final String userAgent, final String baseUrl) {
* @param location The 3 letter ISO country code; must be a country supported by the service (USA, CAN, etc.)
* @param zip The zip/postal code to find headends for
* @return An array of Lineup objects representing all available Lineups for the given zip; never returns null, but may return an empty array
- * @throws InvalidZipCodeException Thrown if the given zip/postal code is invalid
* @throws IOException Thrown if there is any kind of IO error accessing the raw data feed
*/
public final Lineup[] getLineups(final String location, final String zip) throws IOException {
@@ -181,6 +180,14 @@ protected void writeLogoToFile(final Station station, final File dest) throws IO
*/
abstract protected Program fetchProgram(final String progId) throws IOException;
+ /**
+ * Fetches artwork for a single program
+ * @param progId The program id to fetch artwork for
+ * @return The Artwork instance for the given program id or null if unavailable
+ * @throws IOException Thrown on any IO error accessing the data
+ */
+ abstract protected Artwork[] fetchArtwork(final String progId) throws IOException;
+
/**
* Fetch multiple recording schedules in batch.
*
@@ -263,7 +270,7 @@ public String getBaseUrl() {
}
/**
- * @param baseUri the baseUrl to set
+ * @param baseUrl the baseUrl to set
*/
public void setBaseUri(String baseUrl) {
this.baseUrl = baseUrl;
diff --git a/src/main/java/org/schedulesdirect/api/NetworkEpgClient.java b/src/main/java/org/schedulesdirect/api/NetworkEpgClient.java
index 1c72886..c667a89 100644
--- a/src/main/java/org/schedulesdirect/api/NetworkEpgClient.java
+++ b/src/main/java/org/schedulesdirect/api/NetworkEpgClient.java
@@ -18,6 +18,9 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -119,7 +122,7 @@ public NetworkEpgClient(final String id, final String pwd) throws InvalidCredent
}
/**
- * ctor; typically only used for development & testing; allows overriding of JsonRequestFactory instance
+ * ctor; typically only used for development & testing; allows overriding of JsonRequestFactory instance
* @param id
* @param pwd
* @param factory
@@ -145,7 +148,7 @@ public NetworkEpgClient(final String id, final String pwd, final String userAgen
}
/**
- * ctor; typically only used for development & testing; allows overriding of JsonRequestFactory instance
+ * ctor; typically only used for development & testing; allows overriding of JsonRequestFactory instance
* @param id
* @param pwd
* @param userAgent
@@ -165,7 +168,6 @@ public NetworkEpgClient(final String id, final String pwd, final String userAgen
* @param userAgent The user agent to send on all requests to the SD servers
* @param baseUrl The base URL to use for all HTTP communication; most should not set this value as it is for testing and development only!
* @param useCache Should the client instance maintain a cache of created objects or hit the SD server on every request? Though memory intensive, use of the cache is greatly encouraged!
- * @param factory The JsonRequestFactory to be used for this client to generate network requests
* @throws InvalidCredentialsException Thrown if the given credentials were invalid
* @throws IOException Thrown if there is any IO error communicating with the Schedules Direct servers
* @throws ServiceOfflineException Thrown if the web service reports itself as offline/unavailable
@@ -423,6 +425,39 @@ protected Airing[] fetchSchedule(final Station station) throws IOException {
protected Program fetchProgram(final String progId) throws IOException {
return fetchPrograms(new String[] { progId }).values().toArray(new Program[1])[0];
}
+
+ @Override
+ protected Artwork[] fetchArtwork(String progId) throws IOException {
+ String artProgId = progId;
+ if(artProgId.length() > 10) {
+ artProgId = artProgId.substring(0, 10);
+ }
+
+ List aList = new ArrayList<>();
+
+ JSONArray req = new JSONArray();
+ req.put(artProgId);
+
+ JSONArray resp = Config.get().getObjectMapper().readValue(factory.get(DefaultJsonRequest.Action.POST, RestNouns.METADATA, getHash(), getUserAgent(), getBaseUrl()).submitForJson(req), JSONArray.class);
+ for(int i=0; i ids = new ArrayList<>();
diff --git a/src/main/java/org/schedulesdirect/api/Program.java b/src/main/java/org/schedulesdirect/api/Program.java
index 022a030..7717298 100644
--- a/src/main/java/org/schedulesdirect/api/Program.java
+++ b/src/main/java/org/schedulesdirect/api/Program.java
@@ -25,6 +25,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -87,11 +88,11 @@ static public enum ColorCode {
*/
COLOR,
/**
- * The program is in Black & White
+ * The program is in Black & White
*/
BW,
/**
- * The program is in both color and Black & White
+ * The program is in both color and Black & White
*/
COLOR_AND_BW,
/**
@@ -166,6 +167,39 @@ static public enum Role {
VISUAL_EFFECTS
}
+ /**
+ * Represents the entity type that is program represents
+ * @author Ben Fisler
+ *
+ */
+ static public enum EntityType {
+ /**
+ * An unknown value was provided; provide the value in a bug ticket for future inclusion
+ */
+ UNKNOWN,
+ SHOW,
+ EPISODE,
+ SPORTS,
+ MOVIE;
+
+ public static EntityType fromValue(String value) {
+ if(value != null) {
+ switch(value) {
+ case "Show":
+ return SHOW;
+ case "Episode":
+ return EPISODE;
+ case "Sports":
+ return SPORTS;
+ case "Movie":
+ return MOVIE;
+ }
+ }
+
+ return UNKNOWN;
+ }
+ }
+
/**
* Represents a credit in a program
*
@@ -546,8 +580,12 @@ static public String convertToSeriesId(String id) {
private String venue;
private Team[] teams;
private URL[] images;
+ private EntityType entityType;
+ private Map> keywords;
+ private Artwork[] artworks;
private Program seriesInfo;
+
/**
* Consutrctor
* @param src The JSON object from which this instance is being constructed; cannot be null
@@ -687,6 +725,8 @@ else if(o1 == null && o2 == null)
String orig = src.optString("originalAirDate", "");
originalAirDate = orig.length() > 0 && !orig.startsWith("0") ? getOrigAirDateFormat().parse(src.getString("originalAirDate")) : null;
descriptionLanguage = descs != null ? src.optString("descriptionLanguage", null) : null;
+ String entityTypeStr = src.optString("entityType");
+ entityType = entityTypeStr.length() == 0 ? EntityType.UNKNOWN : EntityType.valueOf(entityTypeStr);
String srcType = src.optString("sourceType").toUpperCase();
try {
sourceType = srcType.length() == 0 ? SourceType.NONE : SourceType.valueOf(srcType);
@@ -745,7 +785,37 @@ else if(o1 == null && o2 == null)
}
this.images = urls.toArray(new URL[0]);
}
- seriesInfo = id.startsWith("EP") ? clnt.fetchProgram(convertToSeriesId(id)) : null;
+
+ this.keywords = new LinkedHashMap<>();
+ JSONObject keyWords = src.optJSONObject("keyWords");
+ if(keyWords != null) {
+ Iterator keyWordGroupNamesIt = keyWords.keys();
+ while(keyWordGroupNamesIt.hasNext() == true) {
+ String keyWordGroupName = keyWordGroupNamesIt.next();
+ JSONArray keywords = keyWords.optJSONArray(keyWordGroupName);
+ List keywordGroup = this.keywords.get(keyWordGroupName);
+ if(keywordGroup == null) {
+ keywordGroup = new ArrayList<>();
+ this.keywords.put(keyWordGroupName, keywordGroup);
+ }
+
+ if(keywords != null) {
+ for(int i=0; i < keywords.length(); ++i) {
+ String keyword = keywords.getString(i);
+ keywordGroup.add(keyword);
+ }
+ }
+ }
+ }
+
+ if(src.optBoolean("hasImageArtwork") == true) {
+ artworks = clnt.fetchArtwork(id);
+ }
+ else {
+ artworks = new Artwork[0];
+ }
+
+ seriesInfo = (entityType == EntityType.EPISODE) ? clnt.fetchProgram(convertToSeriesId(id)) : null;
} catch (Throwable t) {
throw new InvalidJsonObjectException(String.format("Program[%s]: %s", id, t.getMessage()), t, src.toString(3));
}
@@ -1184,7 +1254,7 @@ public void setMd5(String md5) {
}
/**
- * @param mpaaRating the mpaaRating to set
+ * @param ratings the ratings to set
*/
public void setRatings(ContentRating[] ratings) {
this.ratings = ratings;
@@ -1388,4 +1458,40 @@ private String toString(Collection> collection, int maxLen) {
builder.append("]");
return builder.toString();
}
+
+ /**
+ * @return the entityType
+ */
+ public EntityType getEntityType() {
+ return entityType;
+ }
+
+ /**
+ * @param entityType the entityType to set
+ */
+ public void setEntityType(EntityType entityType) {
+ this.entityType = entityType;
+ }
+
+ /**
+ * @return the keywords
+ */
+ public Map> getKeywords() {
+ return keywords;
+ }
+
+ /**
+ * @param keywords the keywords to set
+ */
+ public void setKeywords(Map> keywords) {
+ this.keywords = keywords;
+ }
+
+ public Artwork[] getArtworks() {
+ return artworks;
+ }
+
+ public void setArtworks(Artwork[] artworks) {
+ this.artworks = artworks;
+ }
}
diff --git a/src/main/java/org/schedulesdirect/api/RestNouns.java b/src/main/java/org/schedulesdirect/api/RestNouns.java
index f3c0489..4cef1b7 100644
--- a/src/main/java/org/schedulesdirect/api/RestNouns.java
+++ b/src/main/java/org/schedulesdirect/api/RestNouns.java
@@ -31,6 +31,7 @@ public final class RestNouns {
static public final String SCHEDULE_MD5S = SCHEDULES + "/md5";
static public final String MESSAGES = "messages";
static public final String AVAILABLE = "available";
+ static public final String METADATA = "metadata" + "/" + PROGRAMS;
private RestNouns() {}
}
diff --git a/src/main/java/org/schedulesdirect/api/Station.java b/src/main/java/org/schedulesdirect/api/Station.java
index 6340cc1..1e77e7a 100644
--- a/src/main/java/org/schedulesdirect/api/Station.java
+++ b/src/main/java/org/schedulesdirect/api/Station.java
@@ -80,13 +80,13 @@ public void writeImageToFile(File dest) throws IOException {
/**
* @return the url
*/
- URL getUrl() {
+ public URL getUrl() {
return url;
}
/**
* @param url the url to set
*/
- void setUrl(URL url) {
+ public void setUrl(URL url) {
this.url = url;
}
/**
diff --git a/src/main/java/org/schedulesdirect/api/ZipEpgClient.java b/src/main/java/org/schedulesdirect/api/ZipEpgClient.java
index ec4f6c7..0147f64 100644
--- a/src/main/java/org/schedulesdirect/api/ZipEpgClient.java
+++ b/src/main/java/org/schedulesdirect/api/ZipEpgClient.java
@@ -49,16 +49,16 @@
import com.fasterxml.jackson.core.JsonParseException;
/**
- * An implementation of EpgClient that uses a local zip file as its data source
+ * An implementation of EpgClient that uses a local zip file as its data source
*
* The zip file to be used must follow a specific format and structure. Such a zip file
* can be generated by running the EPG Grabber application.
*
- * This implementation has two common uses:
+ *
This implementation has two common uses:
*
* - For development and testing of this API, you can download a zip file of listings data and reuse it instead of constantly hitting the Schedules Direct servers.
* - For production, applications should always simply download a zip file once a day and reuse that zip file via this client implentation, which provides a simple form a caching.
- *
+ *
*
*
* Most every real world application should only be accessing EPG data via instances of this class. Apps should
@@ -110,10 +110,25 @@ static public String scrubFileName(String input) {
return input.replaceAll(INVALID_FILE_CHARS, "_");
}
+ /**
+ * Returns the first 10 characters of a programs ID
+ * @param input The program ID
+ * @return The first 10 characters
+ */
+ static public String artworkId(String input) {
+ String ret = input;
+ if(ret.length() > 10) {
+ ret = ret.substring(0, 10);
+ }
+
+ return ret;
+ }
+
private File src;
private FileSystem vfs;
private Map lineups;
private Map progCache;
+ private Map> artCache;
private boolean closed;
private boolean detailsFetched;
@@ -137,6 +152,7 @@ public ZipEpgClient(final File zip, final String baseUrl) throws IOException {
super(null, baseUrl);
src = zip;
progCache = new HashMap();
+ artCache = new HashMap<>();
URI fsUri;
try {
fsUri = new URI(String.format("jar:%s", zip.toURI()));
@@ -286,6 +302,11 @@ protected Program fetchProgram(final String progId) throws IOException {
Program p = progCache.get(progId);
if(p == null) {
Path path = vfs.getPath(String.format("programs/%s.txt", scrubFileName(progId)));
+
+ if(!Files.exists(path) && progId.startsWith("SH")) {
+ path = vfs.getPath(String.format("seriesInfo/%s.txt", scrubFileName(progId)));
+ }
+
if(Files.exists(path)) {
try(InputStream ins = Files.newInputStream(path)) {
String data = IOUtils.toString(ins, ZIP_CHARSET.toString());
@@ -309,6 +330,49 @@ protected Program fetchProgram(final String progId) throws IOException {
}
return p;
}
+
+ @Override
+ protected Artwork[] fetchArtwork(String progId) throws IOException {
+ if(closed)
+ throw new IllegalStateException("Instance has already been closed!");
+
+ String aId = artworkId(progId);
+
+ List artworks = artCache.get(aId);
+
+ if(artworks == null) {
+ artworks = new ArrayList<>();
+ artCache.put(aId, artworks);
+
+ Path path = vfs.getPath(String.format("artwork/%s.txt", aId));
+ if(Files.exists(path)) {
+ try(InputStream ins = Files.newInputStream(path)) {
+ String data = IOUtils.toString(ins, ZIP_CHARSET.toString());
+ if(data != null) {
+ JSONObject artworkInfo;
+ try {
+ artworkInfo = Config.get().getObjectMapper().readValue(data, JSONObject.class);
+ Object temp = artworkInfo.get("data");
+ if(temp instanceof JSONArray) {
+ JSONArray artworkArr = artworkInfo.getJSONArray("data");
+
+ for(int i=0; i fetchSchedules(final Lineup lineup) throws IOException {
@@ -368,6 +432,7 @@ public void purgeCache() {
if(closed)
throw new IllegalStateException("Instance has already been closed!");
progCache.clear();
+ artCache.clear();
}
@Override
diff --git a/src/main/java/org/schedulesdirect/api/json/DefaultJsonRequest.java b/src/main/java/org/schedulesdirect/api/json/DefaultJsonRequest.java
index 99653d2..d3a54c8 100644
--- a/src/main/java/org/schedulesdirect/api/json/DefaultJsonRequest.java
+++ b/src/main/java/org/schedulesdirect/api/json/DefaultJsonRequest.java
@@ -125,7 +125,6 @@ private Request initRequest() {
/**
* Submit this request; returns the JSON object response received; only call if the request is expected to return a JSON object in response
* @param reqData The supporting data for the request; this is dependent on the action and obj target specified
- * @param ignoreContentType When true, the content type of the response is ignored (handles buggy SD server response for some request types)
* @return The JSON encoded response received from the SD service
* @throws IOException Thrown on any IO error encountered
*/
@@ -167,7 +166,7 @@ public InputStream submitForInputStream(Object reqData) throws IOException {
/**
* Submit this request; returns the raw input stream of the content; caller responsible for closing stream when done.
* @param reqData The supporting data for the request; this is dependent on the action and obj target specified
- * @param failOnStatusError If true and the status code of the HTTP request > 399 then throw an exception; if false just return the entity stream regardless
+ * @param failOnStatusError If true and the status code of the HTTP request > 399 then throw an exception; if false just return the entity stream regardless
* @return The InputStream of data received in response to the request
* @throws IOException Thrown on any IO error encountered
* @throws IllegalStateException Thrown if called on a partially constructed object (the 2 arg ctor)
diff --git a/src/test/java/org/schedulesdirect/api/ArtworkTest.java b/src/test/java/org/schedulesdirect/api/ArtworkTest.java
new file mode 100644
index 0000000..9f89eac
--- /dev/null
+++ b/src/test/java/org/schedulesdirect/api/ArtworkTest.java
@@ -0,0 +1,37 @@
+package org.schedulesdirect.api;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.json.JSONObject;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.schedulesdirect.api.Artwork.Size;
+
+public class ArtworkTest {
+
+ private EpgClient MOCK_CLNT = null;
+
+ @BeforeClass
+ public void setup() {
+ MOCK_CLNT = mock(EpgClient.class);
+ when(MOCK_CLNT.getBaseUrl()).thenReturn("http://testurl.com");
+ }
+
+ @Test
+ public void validateArtworkParse() {
+ String src = "{\"width\": \"135\",\"height\": \"180\",\"uri\": \"assets/p282288_b_v2_aa.jpg\",\"size\": \"Sm\",\"aspect\": \"3x4\",\"category\": \"Banner-L3\",\"text\": \"yes\",\"primary\": \"true\",\"tier\": \"Series\"}";
+ JSONObject o = new JSONObject(src);
+ Artwork a = new Artwork(o, MOCK_CLNT);
+
+ assertEquals(135, a.getWidth());
+ assertEquals(180, a.getHeight());
+ assertTrue(a.getUri().endsWith("assets/p282288_b_v2_aa.jpg"));
+ assertTrue(a.getSize() == Size.SMALL);
+ assertEquals("3x4", a.getAspect());
+ assertEquals("Banner-L3", a.getCategory());
+ assertTrue(a.isText());
+ assertEquals("Series", a.getTier());
+ }
+
+}
diff --git a/src/test/java/org/schedulesdirect/api/ProgramTest.java b/src/test/java/org/schedulesdirect/api/ProgramTest.java
index bb54794..9676ee3 100644
--- a/src/test/java/org/schedulesdirect/api/ProgramTest.java
+++ b/src/test/java/org/schedulesdirect/api/ProgramTest.java
@@ -221,4 +221,15 @@ public void dontAllowDuplicateGenres() throws Exception {
assertEquals(1, p.getGenres().length);
assertEquals("Special", p.getGenres()[0]);
}
+
+ @Test
+ public void validateKeywords() throws Exception {
+ JSONObject src = getRandomSampleProgram();
+ JSONObject keywords = new JSONObject();
+ keywords.put("General", new JSONArray(new String[] {"General1", "General2"}));
+ src.put("keywords", keywords);
+ Program p = new Program(src, CLNT);
+ assertTrue(p.getKeywords().containsKey("General"));
+ assertEquals(2, p.getKeywords().get("General").size());
+ }
}