/*
This file is part of JFLICKS.
JFLICKS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
JFLICKS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with JFLICKS. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jflicks.tv.programdata.sd;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.jflicks.configure.NameValue;
import org.jflicks.db.DbWorker;
import org.jflicks.nms.NMSConstants;
import org.jflicks.tv.Airing;
import org.jflicks.tv.Channel;
import org.jflicks.tv.ChannelLogo;
import org.jflicks.tv.Listing;
import org.jflicks.tv.Show;
import org.jflicks.tv.ShowAiring;
import org.jflicks.tv.programdata.BaseProgramData;
import org.jflicks.util.LogUtil;
import org.jflicks.util.Util;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.Configuration;
import com.db4o.osgi.Db4oService;
import com.db4o.query.Predicate;
import net.sf.xtvdclient.xtvd.datatypes.Duration;
import net.sf.xtvdclient.xtvd.datatypes.Lineup;
import net.sf.xtvdclient.xtvd.datatypes.Program;
import net.sf.xtvdclient.xtvd.datatypes.Schedule;
import net.sf.xtvdclient.xtvd.datatypes.Station;
import net.sf.xtvdclient.xtvd.datatypes.Xtvd;
import net.sf.xtvdclient.xtvd.datatypes.XtvdDate;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
/**
* This class is an implementation of the ProgramData interface using
* Schedules Direct.
*
* @author Doug Barnum
* @version 1.0
*/
public class SchedulesDirectProgramData extends BaseProgramData implements DbWorker {
private ObjectContainer objectContainer;
private Db4oService db4oService;
private boolean overrideTimeToUpdate;
private boolean updatingNow;
/**
* Simple empty constructor.
*/
public SchedulesDirectProgramData() {
setTitle("Schedules Direct");
}
/**
* {@inheritDoc}
*/
public boolean requestUpdate() {
boolean result = false;
if (!isUpdatingNow()) {
result = true;
setOverrideTimeToUpdate(true);
}
return (result);
}
/**
* We use the Db4oService to persist the program data.
*
* @return A Db4oService instance.
*/
public Db4oService getDb4oService() {
return (db4oService);
}
/**
* We use the Db4oService to persist the program data.
*
* @param s A Db4oService instance.
*/
public void setDb4oService(Db4oService s) {
db4oService = s;
}
private boolean isOverrideTimeToUpdate() {
return (overrideTimeToUpdate);
}
private void setOverrideTimeToUpdate(boolean b) {
overrideTimeToUpdate = b;
}
/**
* {@inheritDoc}
*/
public boolean isUpdatingNow() {
return (updatingNow);
}
public void setUpdatingNow(boolean b) {
updatingNow = b;
}
/**
* {@inheritDoc}
*/
public Listing[] getListings() {
Listing[] result = null;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
ObjectSet<Listing> os = oc.queryByExample(Listing.class);
if (os != null) {
result = os.toArray(new Listing[os.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Listing getListingByName(String name) {
Listing result = null;
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (name != null)) {
final String lname = name;
// Should be just one Listing.
List<Listing> listings = oc.query(new Predicate<Listing>() {
public boolean match(Listing l) {
return (lname.equals(l.getName()));
}
});
if ((listings != null) && (listings.size() > 0)) {
result = listings.get(0);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Channel[] getChannels() {
Channel[] result = null;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
ObjectSet<Channel> os = oc.queryByExample(Channel.class);
if (os != null) {
result = os.toArray(new Channel[os.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public ChannelLogo[] getChannelLogos() {
ChannelLogo[] result = null;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
ObjectSet<ChannelLogo> os = oc.queryByExample(ChannelLogo.class);
if (os != null) {
result = os.toArray(new ChannelLogo[os.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Show[] getShows() {
Show[] result = null;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
ObjectSet<Show> os = oc.queryByExample(Show.class);
if (os != null) {
result = os.toArray(new Show[os.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Channel[] getChannelsByListing(Listing l) {
Channel[] result = null;
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (l != null)) {
final String id = l.getId();
if (id != null) {
List<Channel> channels = oc.query(new Predicate<Channel>() {
public boolean match(Channel c) {
return (id.equals(c.getListingId()));
}
});
if ((channels != null) && (channels.size() > 0)) {
result = channels.toArray(new Channel[channels.size()]);
}
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Channel getChannelById(int id, String lid) {
Channel result = null;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
final int cid = id;
final String clid = lid;
// Should be just one Channel.
List<Channel> channels = oc.query(new Predicate<Channel>() {
public boolean match(Channel c) {
// Here we are changing the listingId from an
// exact match to a substring of. The json data
// has not made a nice field that matches the
// listingId exactly. So we are just going to
// check for indexOf to be not -1.
return ((cid == c.getId())
&& (clid.indexOf(c.getListingId()) != -1));
/*
return ((cid == c.getId())
&& (clid.equals(c.getListingId())));
*/
}
});
if ((channels != null) && (channels.size() > 0)) {
result = channels.get(0);
}
}
return (result);
}
private Channel[] getAllChannelsById(int id) {
Channel[] result = null;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
final int cid = id;
List<Channel> channels = oc.query(new Predicate<Channel>() {
public boolean match(Channel c) {
return (cid == c.getId());
}
});
if ((channels != null) && (channels.size() > 0)) {
result = channels.toArray(new Channel[channels.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Show getShowById(String id) {
Show result = null;
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (id != null)) {
final String sid = id;
// Should be just one Show.
List<Show> shows = oc.query(new Predicate<Show>() {
public boolean match(Show s) {
return (sid.equals(s.getId()));
}
});
if ((shows != null) && (shows.size() > 0)) {
result = shows.get(0);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Airing[] getAiringsByChannel(Channel c) {
Airing[] result = null;
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (c != null)) {
final int id = c.getId();
final String lid = c.getListingId();
List<Airing> airings = oc.query(new Predicate<Airing>() {
public boolean match(Airing a) {
return ((id == a.getChannelId())
&& (lid.equals(a.getListingId())));
}
});
if ((airings != null) && (airings.size() > 0)) {
result = airings.toArray(new Airing[airings.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Airing[] getAiringsByShow(Show s) {
Airing[] result = null;
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (s != null)) {
final String id = s.getId();
List<Airing> airings = oc.query(new Predicate<Airing>() {
public boolean match(Airing a) {
return (id.equals(a.getShowId()));
}
});
if ((airings != null) && (airings.size() > 0)) {
result = airings.toArray(new Airing[airings.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public ShowAiring[] getShowAiringsByChannelAndSeriesId(Channel c,
String seriesId) {
ShowAiring[] result = null;
Airing[] airings = getAiringsByChannel(c);
if ((airings != null) && (c != null) && (seriesId != null)) {
ArrayList<ShowAiring> list = new ArrayList<ShowAiring>();
for (int i = 0; i < airings.length; i++) {
Show show = getShowByAiring(airings[i]);
if (show != null) {
if (seriesId.equals(show.getSeriesId())) {
// We have one.
ShowAiring sa = new ShowAiring(show, airings[i]);
list.add(sa);
}
}
}
if (list.size() > 0) {
result = list.toArray(new ShowAiring[list.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public ShowAiring[] getShowAirings(String pattern, int searchType) {
ShowAiring[] result = null;
Show[] shows = getShows(pattern, searchType);
if (shows != null) {
ArrayList<ShowAiring> list = new ArrayList<ShowAiring>();
for (int i = 0; i < shows.length; i++) {
Airing[] airings = getAiringsByShow(shows[i]);
if (airings != null) {
for (int j = 0; j < airings.length; j++) {
ShowAiring sa = new ShowAiring(shows[i], airings[j]);
if (!list.contains(sa)) {
list.add(sa);
}
}
}
}
if (list.size() > 0) {
result = list.toArray(new ShowAiring[list.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Show getShowByAiring(Airing a) {
Show result = null;
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (a != null)) {
final String id = a.getShowId();
List<Show> shows = oc.query(new Predicate<Show>() {
public boolean match(Show show) {
return (id.equals(show.getId()));
}
});
if ((shows != null) && (shows.size() > 0)) {
result = shows.get(0);
}
}
return (result);
}
private Show[] getShows(String pattern, int searchType) {
Show[] result = null;
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (pattern != null)) {
final String pat = pattern.toLowerCase();
final int type = searchType;
List<Show> shows = oc.query(new Predicate<Show>() {
public boolean match(Show s) {
boolean result = false;
String search = null;
switch (type) {
default:
case NMSConstants.SEARCH_TITLE:
case NMSConstants.SEARCH_TITLE_STARTS_WITH:
search = s.getTitle();
break;
case NMSConstants.SEARCH_DESCRIPTION:
search = s.getDescription();
break;
case NMSConstants.SEARCH_TITLE_DESCRIPTION:
search = s.getTitle() + " " + s.getDescription();
break;
}
if (search != null) {
search = search.trim();
if (search.length() > 0) {
search = search.toLowerCase();
if (type == NMSConstants.SEARCH_TITLE_STARTS_WITH) {
result = search.startsWith(pat);
} else {
result = search.indexOf(pat) != -1;
}
}
}
return (result);
}
});
if ((shows != null) && (shows.size() > 0)) {
result = shows.toArray(new Show[shows.size()]);
}
}
return (result);
}
/**
* We have an update to the Schedules Direct data that we persist. He
* we have the opportunity to update our local store with the latest
* guide data.
*
* @param xtvd The object instance containing the Schedules Direct data.
* @param logos An array of ChannelLogo instances.
*/
public void process(Xtvd xtvd, ChannelLogo[] logos) {
LogUtil.log(LogUtil.DEBUG, "process xtvd null? <" + (xtvd == null) + ">");
ObjectContainer oc = getObjectContainer();
if ((oc != null) && (xtvd != null)) {
updateStatus();
ArrayList<Channel> clist = new ArrayList<Channel>();
Map map = xtvd.getStations();
LogUtil.log(LogUtil.DEBUG, "process map null? <" + (map == null) + ">");
if (map != null) {
Collection coll = map.values();
if (coll != null) {
LogUtil.log(LogUtil.DEBUG, "SD station count <" + coll.size() + ">");
purge(oc, Channel.class);
Iterator iter = coll.iterator();
while (iter.hasNext()) {
Station station = (Station) iter.next();
Channel tmp = new Channel();
tmp.setId(station.getId());
tmp.setName(station.getName());
tmp.setFrequency(station.getFccChannelNumber());
tmp.setAffiliate(station.getAffiliate());
tmp.setCallSign(station.getCallSign());
clist.add(tmp);
}
LogUtil.log(LogUtil.DEBUG, "Channel count <" + clist.size() + ">");
}
}
ArrayList<Channel> multiple = new ArrayList<Channel>();
Map<String, Lineup> lmap = xtvd.getLineups();
if (lmap != null) {
Collection<Lineup> coll = lmap.values();
if (coll != null) {
purge(oc, Listing.class);
// First go through the lineups and set the proper
// reference number. A problem here could be that
// the user doesn't have an OTA lineup which would
// probably break things. I guess we will have to
// use the callsign as a backup.
Iterator<Lineup> iter = coll.iterator();
int count = 0;
while (iter.hasNext()) {
Lineup lineup = iter.next();
count += processReferenceChannels(lineup, clist);
}
LogUtil.log(LogUtil.DEBUG, "Found <" + count + "> channels to reference");
LogUtil.log(LogUtil.DEBUG, "Channel count <" + clist.size() + ">");
// Now we go through again and if we have
// set the reference number we should be
// good to go.
iter = coll.iterator();
while (iter.hasNext()) {
Lineup lineup = iter.next();
Listing listing =
processLineup(lineup, clist, multiple);
oc.store(listing);
}
oc.commit();
}
}
// We have built all the fields to the Channels we know about.
// Now lets write then to the DB.
LogUtil.log(LogUtil.DEBUG, "Multiple Channel count <" + multiple.size() + ">");
for (int i = 0; i < multiple.size(); i++) {
Channel tmp = multiple.get(i);
oc.store(tmp);
}
oc.commit();
// Just put in our given ChannelLogo instances.
if (logos != null) {
purge(oc, ChannelLogo.class);
for (int i = 0; i < logos.length; i++) {
oc.store(logos[i]);
}
}
// Process the Programs and put new Show instances into the
// database.
Map<String, Program> pmap = xtvd.getPrograms();
LogUtil.log(LogUtil.DEBUG, "Program map <" + pmap + ">");
if (pmap != null) {
LogUtil.log(LogUtil.DEBUG, "Program map count <" + pmap.size() + ">");
Collection<Program> coll = pmap.values();
if (coll != null) {
LogUtil.log(LogUtil.DEBUG, "Program count <" + coll.size() + ">");
purge(oc, Show.class);
Iterator<Program> iter = coll.iterator();
while (iter.hasNext()) {
Program p = iter.next();
Show show = new Show();
show.setId(p.getId());
show.setTitle(p.getTitle());
show.setSubtitle(p.getSubtitle());
show.setDescription(p.getDescription());
show.setType(p.getShowType());
show.setEpisodeNumber(p.getSyndicatedEpisodeNumber());
XtvdDate xdate = p.getOriginalAirDate();
if (xdate != null) {
show.setOriginalAirDate(xdate.getDate());
}
show.setSeriesId(p.getSeries());
oc.store(show);
}
oc.commit();
}
}
// Next we process the schedules and put Airings into the db.
Collection<Schedule> scheds = xtvd.getSchedules();
if ((scheds != null) && (scheds.size() > 0)) {
LogUtil.log(LogUtil.DEBUG, "Schedule count <" + scheds.size() + ">");
purge(oc, Airing.class);
Iterator<Schedule> iter = scheds.iterator();
while (iter.hasNext()) {
Schedule s = iter.next();
Channel[] array = getAllChannelsById(s.getStation());
if (array != null) {
for (int i = 0; i < array.length; i++) {
Airing airing = new Airing();
airing.setShowId(s.getProgram());
airing.setChannelId(array[i].getId());
airing.setListingId(array[i].getListingId());
airing.setAirDateUTC(s.getTime().getDate());
Duration dur = s.getDuration();
if (dur != null) {
int hours =
Util.str2int(dur.getHours(), 0) * 3600;
int mins =
Util.str2int(dur.getMinutes(), 0) * 60;
airing.setDuration((long) (hours + mins));
oc.store(airing);
}
}
}
}
oc.commit();
}
LogUtil.log(LogUtil.INFO, "Schedules Direct data process complete!");
// Let others know...
fireDataUpdateEvent();
}
}
private int processReferenceChannels(Lineup l, ArrayList<Channel> list) {
int result = 0;
if ((l != null) && (list != null)) {
Collection<net.sf.xtvdclient.xtvd.datatypes.Map> coll = l.getMaps();
if (coll != null) {
Iterator<net.sf.xtvdclient.xtvd.datatypes.Map> iter =
coll.iterator();
while (iter.hasNext()) {
net.sf.xtvdclient.xtvd.datatypes.Map map = iter.next();
Channel found = findChannel(list, map.getStation());
if (found != null) {
if (found.getFrequency() != 0) {
int minor = map.getChannelMinor();
if (minor != 0) {
found.setNumber(map.getChannel() + "." + minor);
result++;
}
}
}
}
}
}
return (result);
}
private Listing processLineup(Lineup l, ArrayList<Channel> list,
ArrayList<Channel> all) {
Listing result = null;
if ((l != null) && (list != null) && (all != null)) {
result = new Listing();
result.setName(l.getName());
result.setId(l.getId());
LogUtil.log(LogUtil.DEBUG, "Listing name <" + result.getName() + ">");
LogUtil.log(LogUtil.DEBUG, "Listing id <" + result.getId() + ">");
Collection<net.sf.xtvdclient.xtvd.datatypes.Map> coll = l.getMaps();
if (coll != null) {
Iterator<net.sf.xtvdclient.xtvd.datatypes.Map> iter =
coll.iterator();
while (iter.hasNext()) {
net.sf.xtvdclient.xtvd.datatypes.Map map = iter.next();
Channel found = findChannel(list, map.getStation());
if (found != null) {
LogUtil.log(LogUtil.DEBUG, "Channel found " + found);
Channel copy = new Channel(found);
copy.setListingId(l.getId());
if (copy.getFrequency() != 0) {
int minor = map.getChannelMinor();
if (minor != 0) {
copy.setNumber(map.getChannel() + "." + minor);
copy.setReferenceNumber(copy.getNumber());
} else {
copy.setNumber(map.getChannel());
copy.setReferenceNumber(found.getNumber());
}
} else {
copy.setNumber(map.getChannel());
copy.setReferenceNumber(found.getNumber());
}
all.add(copy);
} else {
LogUtil.log(LogUtil.DEBUG, "Channel not found " + map.getStation());
}
}
}
}
return (result);
}
private Channel findChannel(ArrayList<Channel> list, int id) {
Channel result = null;
if (list != null) {
for (int i = 0; i < list.size(); i++) {
Channel tmp = list.get(i);
if (tmp != null) {
if (tmp.getId() == id) {
result = tmp;
break;
}
}
}
}
return (result);
}
private synchronized ObjectContainer getObjectContainer() {
if (objectContainer == null) {
Db4oService s = getDb4oService();
if (s != null) {
Configuration config = s.newConfiguration();
config.objectClass(
Airing.class).objectField("id").indexed(true);
config.objectClass(
Airing.class).objectField("channelId").indexed(true);
config.objectClass(
Channel.class).objectField("id").indexed(true);
config.objectClass(
Channel.class).objectField("listingId").indexed(true);
config.objectClass(Show.class).objectField("id").indexed(true);
config.objectClass(
Show.class).objectField("title").indexed(true);
config.objectClass(
Show.class).objectField("description").indexed(true);
objectContainer = s.openFile(config, "db/sd.dat");
}
}
return (objectContainer);
}
/**
* Close up all resources.
*/
public void close() {
if (objectContainer != null) {
boolean result = objectContainer.close();
LogUtil.log(LogUtil.DEBUG, "SchedulesDirectProgramData: closed " + result);
objectContainer = null;
} else {
LogUtil.log(LogUtil.DEBUG, "SchedulesDirectProgramData: Tried to close "
+ "but objectContainer null.");
}
}
private void purge(ObjectContainer db, Class c) {
if ((db != null) && (c != null)) {
ObjectSet result = db.queryByExample(c);
while (result.hasNext()) {
db.delete(result.next());
}
}
}
/**
* {@inheritDoc}
*/
public long getNextTimeToRun() {
long result = -1L;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
ObjectSet<Status> os = oc.queryByExample(Status.class);
if (os != null) {
if (os.size() > 0) {
Status status = os.next();
result = status.getNextUpdate();
}
}
}
return (result);
}
/**
* We compute if it's time to update data from Schedules Direct. We
* should update once a day.
*
* @return True if it's time to get more data.
*/
public boolean isTimeToUpdate() {
boolean result = false;
ObjectContainer oc = getObjectContainer();
if (oc != null) {
Status status = null;
if (isOverrideTimeToUpdate()) {
setOverrideTimeToUpdate(false);
status = new Status();
status.setNextUpdate(System.currentTimeMillis());
oc.store(status);
oc.commit();
LogUtil.log(LogUtil.INFO, "Time to update by override!");
result = true;
} else {
ObjectSet<Status> os = oc.queryByExample(Status.class);
if (os != null) {
if (os.size() > 0) {
status = os.next();
long now = System.currentTimeMillis();
long next = status.getNextUpdate();
if (now > next) {
result = true;
LogUtil.log(LogUtil.INFO, "Time to update! Now is newer!");
} else {
LogUtil.log(LogUtil.INFO, "Not time to update: " + new Date(next));
}
} else {
status = new Status();
status.setNextUpdate(System.currentTimeMillis());
oc.store(status);
oc.commit();
LogUtil.log(LogUtil.INFO, "Time to update! No history...");
result = true;
}
} else {
status = new Status();
status.setNextUpdate(System.currentTimeMillis());
oc.store(status);
oc.commit();
LogUtil.log(LogUtil.INFO, "Time to update! No history...");
result = true;
}
}
}
return (result);
}
private void updateStatus() {
ObjectContainer oc = getObjectContainer();
if (oc != null) {
ObjectSet<Status> os = oc.queryByExample(Status.class);
if (os.size() > 0) {
Status status = os.next();
status.setLastUpdate(status.getNextUpdate());
// We want to update next tomorrow at around the
// preferred update hour.
int hcount = 24;
int hour = getConfiguredUpdateHour();
Calendar now = Calendar.getInstance();
int hod = now.get(Calendar.HOUR_OF_DAY);
if (hour > hod) {
hcount = 24 + (hour - hod);
} else if (hod > hour) {
hcount = 24 - (hod - hour);
}
status.setNextUpdate(System.currentTimeMillis()
+ (1000 * 3600 * hcount));
purge(oc, Status.class);
oc.store(status);
oc.commit();
}
}
}
}