/*
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.scheduler;
import java.io.File;
import java.io.Serializable;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import org.jflicks.configure.BaseConfig;
import org.jflicks.configure.Configuration;
import org.jflicks.configure.NameValue;
import org.jflicks.nms.NMS;
import org.jflicks.nms.NMSConstants;
import org.jflicks.tv.Airing;
import org.jflicks.tv.Channel;
import org.jflicks.tv.Recording;
import org.jflicks.tv.RecordingRule;
import org.jflicks.tv.Show;
import org.jflicks.tv.ShowAiring;
import org.jflicks.tv.Upcoming;
import org.jflicks.tv.recorder.Recorder;
import org.jflicks.util.LogUtil;
/**
* This class is a base implementation of the Scheduler interface.
*
* @author Doug Barnum
* @version 1.0
*/
public abstract class BaseScheduler extends BaseConfig implements Scheduler {
private NMS nms;
private String title;
private ArrayList<PendingRecord> pendingRecordList;
private ArrayList<PendingRecord> workPendingRecordList;
private int robinIndex;
/**
* Simple empty constructor.
*/
public BaseScheduler() {
setPendingRecordList(new ArrayList<PendingRecord>());
setWorkPendingRecordList(new ArrayList<PendingRecord>());
}
/**
* {@inheritDoc}
*/
public String getTitle() {
return (title);
}
/**
* Convenience method to set this property.
*
* @param s The given title value.
*/
public void setTitle(String s) {
title = s;
}
/**
* {@inheritDoc}
*/
public NMS getNMS() {
return (nms);
}
/**
* {@inheritDoc}
*/
public void setNMS(NMS n) {
nms = n;
}
/**
* {@inheritDoc}
*/
public String[] getConfiguredListingNames() {
String[] result = null;
Configuration c = getConfiguration();
if (c != null) {
NameValue[] array = c.getNameValues();
if (array != null) {
ArrayList<String> l = new ArrayList<String>();
for (int i = 0; i < array.length; i++) {
String desc = array[i].getDescription();
if ((desc != null) && (desc.equals(NMSConstants.RECORDING_DEVICE))) {
String tmp = array[i].getValue();
if ((tmp != null) && (!tmp.equals(NMSConstants.NOT_CONNECTED))) {
if (!l.contains(tmp)) {
l.add(tmp);
}
}
}
}
if (l.size() > 0) {
result = l.toArray(new String[l.size()]);
}
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Recorder[] getConfiguredRecorders() {
Recorder[] result = null;
NMS n = getNMS();
Configuration c = getConfiguration();
if ((c != null) && (n != null)) {
NameValue[] array = c.getNameValues();
if (array != null) {
ArrayList<Recorder> l = new ArrayList<Recorder>();
for (int i = 0; i < array.length; i++) {
String desc = array[i].getDescription();
if ((desc != null) && (desc.equals(NMSConstants.RECORDING_DEVICE))) {
String tmp = array[i].getValue();
if ((tmp != null) && (!tmp.equals(NMSConstants.NOT_CONNECTED))) {
// Ok found a connected Recorder. We need to get
// the device from the name. It should be the
// last token.
String name = array[i].getName();
if (name != null) {
name = name.substring(name.lastIndexOf(" "));
name = name.trim();
Recorder rec = n.getRecorderByDevice(name);
if (rec != null) {
l.add(rec);
}
}
}
}
}
if (l.size() > 0) {
// Before we go back we want to put our "preferred" recorders first.
ArrayList<Recorder> plist = new ArrayList<Recorder>();
for (int i = 0; i < l.size(); i++) {
Recorder r = l.get(i);
if (r.isPreferred()) {
plist.add(r);
}
}
for (int i = 0; i < l.size(); i++) {
Recorder r = l.get(i);
if (!r.isPreferred()) {
plist.add(r);
}
}
result = plist.toArray(new Recorder[plist.size()]);
}
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public String[] getConfiguredRecordingDirectories() {
String[] result = null;
Configuration c = getConfiguration();
if (c != null) {
NameValue nv =
c.findNameValueByName(NMSConstants.RECORDING_DIRECTORIES);
if (nv != null) {
result = nv.valueToArray();
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Channel[] getRecordableChannels() {
Channel[] result = null;
Recorder[] rarray = getConfiguredRecorders();
if ((rarray != null) && (rarray.length > 0)) {
ArrayList<Channel> clist = new ArrayList<Channel>();
for (int i = 0; i < rarray.length; i++) {
Channel[] carray = getChannelsByRecorder(rarray[i]);
if ((carray != null) && (carray.length > 0)) {
for (int j = 0; j < carray.length; j++) {
if (!clist.contains(carray[j])) {
clist.add(carray[j]);
}
}
}
}
if (clist.size() > 0) {
result = clist.toArray(new Channel[clist.size()]);
Arrays.sort(result);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public String getListingNameByRecorder(Recorder r) {
String result = null;
Configuration c = getConfiguration();
NMS n = getNMS();
if ((c != null) && (n != null) && (r != null)) {
NameValue[] array = c.getNameValues();
if (array != null) {
for (int i = 0; i < array.length; i++) {
String desc = array[i].getDescription();
if ((desc != null)
&& (desc.equals(NMSConstants.RECORDING_DEVICE))) {
String tmp = array[i].getValue();
if ((tmp != null)
&& (!tmp.equals(NMSConstants.NOT_CONNECTED))) {
// Ok found a connected Recorder. We need to get
// the device from the name. It should be the
// last token.
String dev = array[i].getName();
if (dev != null) {
dev = dev.substring(dev.lastIndexOf(" "));
dev = dev.trim();
if (dev.equals(r.getDevice())) {
// Ok found the recorder.
result = tmp;
break;
}
}
}
}
}
}
}
return (result);
}
protected Channel[] getChannelsByRecorder(Recorder r) {
Channel[] result = null;
Configuration c = getConfiguration();
NMS n = getNMS();
if ((c != null) && (n != null) && (r != null)) {
NameValue[] array = c.getNameValues();
if (array != null) {
for (int i = 0; i < array.length; i++) {
String desc = array[i].getDescription();
if ((desc != null)
&& (desc.equals(NMSConstants.RECORDING_DEVICE))) {
String tmp = array[i].getValue();
if ((tmp != null)
&& (!tmp.equals(NMSConstants.NOT_CONNECTED))) {
// Ok found a connected Recorder. We need to get
// the device from the name. It should be the
// last token.
String dev = array[i].getName();
if (dev != null) {
dev = dev.substring(dev.lastIndexOf(" "));
dev = dev.trim();
if (dev.equals(r.getDevice())) {
// Ok found the recorder.
result = n.getChannelsByListingName(tmp);
// The result is all the channels that
// are defined by the listing. However
// this particular Recorder may really
// just support a subset of channels,
// or have a list of channels that it
// doesn't support.
result = r.getCustomChannels(result);
break;
}
}
}
}
}
}
}
return (result);
}
protected ArrayList<PendingRecord> getPendingRecordList() {
return (pendingRecordList);
}
private void setPendingRecordList(ArrayList<PendingRecord> l) {
pendingRecordList = l;
}
private ArrayList<PendingRecord> getWorkPendingRecordList() {
return (workPendingRecordList);
}
private void setWorkPendingRecordList(ArrayList<PendingRecord> l) {
workPendingRecordList = l;
}
/**
* {@inheritDoc}
*/
public PendingRecord[] getReadyPendingRecords() {
PendingRecord[] result = null;
ArrayList<PendingRecord> list = getPendingRecordList();
if ((list != null) && (list.size() > 0)) {
ArrayList<PendingRecord> readylist = null;
long time = System.currentTimeMillis() + 60000;
int count = 0;
for (int i = 0; i < list.size(); i++) {
PendingRecord pr = list.get(i);
if (pr.getStart() < time) {
// Time has come to get rid of this PendingRecord,
// whether it's going to be recorded or not.
count++;
// Now if it's ready, add it to our ready list.
if (pr.isReadyStatus()) {
if (readylist == null) {
readylist = new ArrayList<PendingRecord>();
}
readylist.add(pr);
}
}
}
// Remove the ones that have time expired...
if (count > 0) {
for (int i = 0; i < count; i++) {
list.remove(0);
}
}
if (readylist != null) {
for (int i = 0; i < readylist.size(); i++) {
PendingRecord pr = readylist.get(i);
if (pr != null) {
addRecordedShow(new RecordedShow(pr.getShowId()));
Recording rec = pr.getRecording();
if (rec != null) {
rec.setCurrentlyRecording(true);
addRecording(rec);
}
/*
* Note we will remove this rule when indexer is called.
RecordingRule rr = pr.getRecordingRule();
if ((rr != null) && (rr.isOnceType())) {
removeRecordingRule(rr);
}
*/
}
}
result = readylist.toArray(new PendingRecord[readylist.size()]);
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public File createFile(PendingRecord pr) {
File result = null;
String[] array = getConfiguredRecordingDirectories();
if ((array != null) && (array.length > 0)) {
File dir = null;
int checks = 0;
// Make sure the directory has at least 6-gig available...
while (checks < array.length) {
// We keep a robinIndex to rotate around directories.
if (robinIndex >= array.length) {
robinIndex = 0;
}
// Use the current directory and then incr.
dir = new File(array[robinIndex++]);
if (dir.getUsableSpace() > 16442450944L) {
// We are done.
checks = array.length;
} else {
LogUtil.log(LogUtil.INFO, "Skipping " + dir
+ " as there is not enough room.");
checks++;
dir = null;
}
}
if (dir != null) {
StringBuffer sb = new StringBuffer();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MM_dd_HH_mm");
sdf.format(new Date(pr.getStart()), sb, new FieldPosition(0));
Recorder rec = pr.getRecorder();
if (rec != null) {
String ext = rec.getExtension();
if (ext != null) {
sb.append("." + ext);
} else {
sb.append(".mpg");
}
} else {
sb.append(".mpg");
}
sb.insert(0, pr.getShowId() + "_");
result = new File(dir, sb.toString());
} else {
LogUtil.log(LogUtil.WARNING, "No configured directories have space!");
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public Upcoming[] getUpcomings() {
Upcoming[] result = null;
ArrayList<PendingRecord> list = getPendingRecordList();
if ((list != null) && (list.size() > 0)) {
String[] priorities = RecordingRule.getPriorityNames();
result = new Upcoming[list.size()];
for (int i = 0; i < list.size(); i++) {
PendingRecord pr = list.get(i);
RecordingRule rr = pr.getRecordingRule();
result[i] = new Upcoming();
result[i].setShowId(pr.getShowId());
if (rr != null) {
result[i].setTitle(rr.getName());
result[i].setPriority(priorities[rr.getPriority()]);
} else {
result[i].setTitle("unknown");
result[i].setPriority("unknown");
}
Recording rec = pr.getRecording();
if (rec != null) {
result[i].setSubtitle(rec.getSubtitle());
result[i].setDescription(rec.getDescription());
result[i].setSeriesId(rec.getSeriesId());
result[i].setDate(rec.getDate());
} else {
result[i].setSubtitle("unknown");
result[i].setDescription("unknown");
}
Channel c = pr.getChannel();
if (c != null) {
result[i].setChannelNumber(c.getNumber());
result[i].setChannelName(c.getName());
} else {
result[i].setChannelNumber("unknown");
result[i].setChannelName("unknown");
}
result[i].setDuration(((pr.getDuration() + 1) / 60)
+ " minutes");
Date d = new Date(pr.getStart());
result[i].setStart(d.toString());
result[i].setStatus(pr.getStatusAsString());
Recorder r = pr.getRecorder();
if (r != null) {
result[i].setRecorderName(r.getTitle() + " "
+ r.getDevice());
}
}
}
return (result);
}
/**
* {@inheritDoc}
*/
public void requestRescheduling() {
// For now we just do it. Perhaps in the future we might need
// to take care in case we have different users asking at the
// same time.
updatePendingRecords();
}
protected synchronized void updatePendingRecords() {
LogUtil.log(LogUtil.INFO, "Running updatePendingRecords");
ArrayList<PendingRecord> workList = getWorkPendingRecordList();
NMS n = getNMS();
Recorder[] recs = getConfiguredRecorders();
if ((workList != null) && (n != null) && (recs != null)) {
LogUtil.log(LogUtil.DEBUG, "CONFIGURED RECORDERS COUNT: " + recs.length);
RecorderInformation[] ris = new RecorderInformation[recs.length];
for (int i = 0; i < ris.length; i++) {
ris[i] = new RecorderInformation();
ris[i].setRecorder(recs[i]);
ris[i].setChannels(getChannelsByRecorder(recs[i]));
if (recs[i].isRecording()) {
LogUtil.log(LogUtil.DEBUG, "recorder: " + recs[i]
+ " is recording now...adding time range");
long started = recs[i].getStartedAt();
TimeRange tr = new TimeRange(started,
started + (recs[i].getDuration() * 1000));
ris[i].addTimeRange(tr);
} else {
LogUtil.log(LogUtil.DEBUG, "recorder " + recs[i] + " is NOT recording now");
}
}
workList.clear();
RecordingRule[] rules = getRecordingRules();
if (rules != null) {
// Now we need to sort the rules by priority so higher
// priority rules get done first.
for (int i = 0; i < rules.length; i++) {
rules[i].setSortBy(RecordingRule.SORT_BY_PRIORITY);
}
Arrays.sort(rules);
for (int i = 0; i < rules.length; i++) {
// Right now we just have ONCE or SERIES recording types.
// Perhaps more in the future but for now...first see
// what channel we are talking about...
Channel chan = n.getChannelById(rules[i].getChannelId(), rules[i].getListingId());
LogUtil.log(LogUtil.DEBUG, "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
LogUtil.log(LogUtil.DEBUG, "chan: " + chan);
LogUtil.log(LogUtil.DEBUG, "id: " + rules[i].getChannelId());
LogUtil.log(LogUtil.DEBUG, "id: " + rules[i].getListingId());
LogUtil.log(LogUtil.DEBUG, "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
if (chan != null) {
long now = System.currentTimeMillis();
ShowAiring[] sas = null;
int type = rules[i].getType();
if (type == RecordingRule.ONCE_TYPE) {
ShowAiring sa = rules[i].getShowAiring();
if (sa != null) {
if (!sa.isOver()) {
sas = new ShowAiring[1];
sas[0] = sa;
// Even record if it's been done before.
removeRecordedShow(
new RecordedShow(sa.getShow()));
} else {
removeRecordingRule(rules[i]);
}
} else {
LogUtil.log(LogUtil.DEBUG, "We have a ONCE but no ShowAiring!");
}
} else if (type == RecordingRule.SERIES_TYPE) {
sas = getShowAiringsByChannelAndSeriesId(chan,
rules[i].getSeriesId());
}
if (sas != null) {
Arrays.sort(sas);
for (int j = 0; j < sas.length; j++) {
Airing air = sas[j].getAiring();
Show show = sas[j].getShow();
if ((air != null) && (show != null)) {
Date date = air.getAirDate();
if (date != null) {
long slen = air.getDuration() * 1000;
if (now < date.getTime()) {
// This hasn't happended yet so
// put er in.
PendingRecord pr = new PendingRecord();
pr.setRecordingRule(rules[i]);
pr.setName(show.getTitle());
pr.setShowId(show.getId());
pr.setChannel(chan);
pr.setDuration(air.getDuration() - 10 + rules[i].getEndPadding());
pr.setStart(date.getTime() + (rules[i].getBeginPadding() * 1000));
Recording rec = new Recording(sas[j]);
pr.setRecording(rec);
rec.setRecordingRuleId(pr.getRecordingRule().getId());
workList.add(pr);
} else if (now < (date.getTime() + slen)) {
// This could be just requested
// to record. We only put it
// in if it's not currently being
// recorded now.
if (!isRecordingNow(sas[j])) {
PendingRecord pr = new PendingRecord();
pr.setRecordingRule(rules[i]);
pr.setName(show.getTitle());
pr.setShowId(show.getId());
pr.setChannel(chan);
pr.setDuration(air.getDuration() - 10 + rules[i].getEndPadding());
pr.setStart(date.getTime() + (rules[i].getBeginPadding() * 1000));
Recording rec = new Recording(sas[j]);
rec.setRecordingRuleId(pr.getRecordingRule().getId());
pr.setRecording(rec);
workList.add(pr);
}
}
} else {
LogUtil.log(LogUtil.DEBUG, "We have an Airing but "
+ " the Date is NULL.");
}
} else {
LogUtil.log(LogUtil.DEBUG, "We have a ShowAiring but "
+ " the Show or Airing is NULL.");
}
}
}
}
}
}
// At this point we have all our PendingRecord instances created.
// Now we work on eliminating them if we have recorded them
// before or if we don't have a recorder available.
LogUtil.log(LogUtil.DEBUG, "pending record count: " + workList.size()
+ " before checking whether previous recorded");
for (int i = 0; i < workList.size(); i++) {
PendingRecord pr = workList.get(i);
if (isAlreadyRecorded(pr.getShowId())) {
pr.setStatus(PendingRecord.PREVIOUS_RECORD);
}
}
// Next we need to update the "laterAvailable" and
// "earlierAvailable" flags to help us sort it out later.
checkDuplicates(workList);
// Next we need to assign a Recorder. Here the "status" could
// turn to be a "conflict" or "later".
for (int i = 0; i < workList.size(); i++) {
PendingRecord pr = workList.get(i);
if ((!pr.isEarlierStatus()) && (!pr.isPreviousRecordStatus())) {
Channel channel = pr.getChannel();
long start = pr.getStart();
long end = start + (pr.getDuration() * 1000);
TimeRange tr = new TimeRange(start, end);
for (int j = 0; j < ris.length; j++) {
if (ris[j].supports(channel)) {
if (!ris[j].isBusyAt(tr)) {
ris[j].addTimeRange(tr);
pr.setRecorder(ris[j].getRecorder());
File f = createFile(pr);
pr.setFile(f);
Recording rec = pr.getRecording();
rec.setPath(f.getPath());
pr.setStatus(PendingRecord.READY);
// Now lets flag any duplicates there
// might be since we have this recording
// covered here.
if (pr.isLaterAvailable()) {
flagDuplicates(workList, pr);
}
break;
} else {
LogUtil.log(LogUtil.DEBUG, "recorder busy: " + ris[j]);
}
} else {
LogUtil.log(LogUtil.DEBUG, "supports channel failed...");
}
}
}
if (pr.isUndeterminedStatus()) {
if (pr.isLaterAvailable()) {
pr.setStatus(PendingRecord.LATER);
} else {
pr.setStatus(PendingRecord.CONFLICT);
}
}
}
for (int i = 0; i < workList.size(); i++) {
PendingRecord pr = workList.get(i);
if (pr.isUndeterminedStatus()) {
LogUtil.log(LogUtil.WARNING, "PROBLEM: Should all be solved by now");
}
}
// So at this point the working list should be all set.
// So clear the regular list and copy over the working one.
ArrayList<PendingRecord> realList = getPendingRecordList();
if (realList != null) {
synchronized (realList) {
Collections.sort(workList);
realList.clear();
realList.addAll(workList);
}
dump();
n.sendMessage(NMSConstants.MESSAGE_SCHEDULE_UPDATE);
}
}
}
private boolean isRecordingNow(ShowAiring sa) {
boolean result = false;
if (sa != null) {
Airing airing = sa.getAiring();
Recorder[] array = getConfiguredRecorders();
if ((array != null) && (airing != null)) {
int cid = airing.getChannelId();
for (int i = 0; i < array.length; i++) {
if (array[i].isRecording()) {
Channel c = array[i].getChannel();
if (c != null) {
if (cid == c.getId()) {
result = true;
break;
}
}
}
}
}
}
LogUtil.log(LogUtil.DEBUG, "isRecordingNow: " + sa + " " + result);
return (result);
}
private void flagDuplicates(ArrayList<PendingRecord> list,
PendingRecord pr) {
if ((list != null) && (pr != null)) {
String showId = pr.getShowId();
if ((showId != null) && (!pr.isOnceType())) {
for (int i = 0; i < list.size(); i++) {
PendingRecord tmp = list.get(i);
if ((tmp != pr) && (!tmp.isOnceType())) {
if (showId.equals(tmp.getShowId())) {
tmp.setStatus(PendingRecord.EARLIER);
}
}
}
}
}
}
private void checkDuplicates(ArrayList<PendingRecord> list) {
// We cannot assume the list is sorted in anyway so we have to
// collect all duplicated by searching through the list.
if ((list != null) && (list.size() > 1)) {
ArrayList<PendingRecord> duplist = new ArrayList<PendingRecord>();
for (int i = 0; i < list.size(); i++) {
PendingRecord pr0 = list.get(i);
String showId = pr0.getShowId();
if ((pr0.isUndeterminedStatus()) && (showId != null)) {
duplist.clear();
//for (int j = 0; j < list.size(); j++) {
for (int j = i + 1; j < list.size(); j++) {
if (i != j) {
PendingRecord pr1 = list.get(j);
if (!pr1.isOnceType()) {
if (pr1.isUndeterminedStatus()) {
if (showId.equals(pr1.getShowId())) {
if (!duplist.contains(pr0)) {
duplist.add(pr0);
}
if (!duplist.contains(pr1)) {
duplist.add(pr1);
}
}
}
}
}
}
if (duplist.size() > 0) {
Collections.sort(duplist);
for (int k = 0; k < duplist.size(); k++) {
if (k == 0) {
// First item in the list.
duplist.get(k).setLaterAvailable(true);
duplist.get(k).setEarlierAvailable(false);
} else if ((k + 1) < duplist.size()) {
// A middle item.
duplist.get(k).setLaterAvailable(true);
duplist.get(k).setEarlierAvailable(true);
} else {
// This is the last item in the list.
duplist.get(k).setLaterAvailable(false);
duplist.get(k).setEarlierAvailable(true);
}
}
}
}
}
}
}
private void dump() {
ArrayList<PendingRecord> realList = getPendingRecordList();
if (realList != null) {
int readyCount = 0;
for (int i = 0; i < realList.size(); i++) {
PendingRecord pr = realList.get(i);
LogUtil.log(LogUtil.DEBUG, "Channel: " + pr.getChannel());
LogUtil.log(LogUtil.DEBUG, "Duration: " + pr.getDuration());
LogUtil.log(LogUtil.DEBUG, "File: " + pr.getFile());
LogUtil.log(LogUtil.DEBUG, "Name: " + pr.getName());
LogUtil.log(LogUtil.DEBUG, "Recorder: " + pr.getRecorder());
LogUtil.log(LogUtil.DEBUG, "ShowId: " + pr.getShowId());
LogUtil.log(LogUtil.DEBUG, "Start: " + new Date(pr.getStart()));
LogUtil.log(LogUtil.DEBUG, "Status: " + pr.getStatus());
LogUtil.log(LogUtil.DEBUG, "-----------------------------------");
if (pr.isReadyStatus()) {
readyCount++;
}
}
LogUtil.log(LogUtil.INFO, "There are " + readyCount + " recordings scheduled.");
}
}
static class PendingRecordByTime implements Comparator<PendingRecord>,
Serializable {
public int compare(PendingRecord pr0, PendingRecord pr1) {
Long l0 = Long.valueOf(pr0.getStart());
Long l1 = Long.valueOf(pr1.getStart());
return (l0.compareTo(l1));
}
}
}