Create and fill show instances when pypo requests the schedule Changed getCcShowInstancess function to return all instances Changed some function calls to retrieve only show instances scheduled in the future
1120 lines
41 KiB
PHP
1120 lines
41 KiB
PHP
<?php
|
|
|
|
class Application_Model_Schedule
|
|
{
|
|
/**
|
|
* Return TRUE if file is going to be played in the future.
|
|
*
|
|
* @param string $p_fileId
|
|
*/
|
|
public static function IsFileScheduledInTheFuture($p_fileId)
|
|
{
|
|
$sql = <<<SQL
|
|
SELECT COUNT(*)
|
|
FROM cc_schedule
|
|
WHERE file_id = :file_id
|
|
AND ends > NOW() AT TIME ZONE 'UTC'
|
|
SQL;
|
|
$count = Application_Common_Database::prepareAndExecute( $sql, array(
|
|
':file_id'=>$p_fileId), 'column');
|
|
return (is_numeric($count) && ($count != '0'));
|
|
}
|
|
|
|
public static function getAllFutureScheduledFiles($instanceId=null)
|
|
{
|
|
$sql = <<<SQL
|
|
SELECT distinct(file_id)
|
|
FROM cc_schedule
|
|
WHERE ends > now() AT TIME ZONE 'UTC'
|
|
AND file_id is not null
|
|
SQL;
|
|
|
|
$files = Application_Common_Database::prepareAndExecute( $sql, array());
|
|
|
|
$real_files = array();
|
|
foreach ($files as $f) {
|
|
$real_files[] = $f['file_id'];
|
|
}
|
|
|
|
return $real_files;
|
|
}
|
|
|
|
public static function getAllFutureScheduledWebstreams()
|
|
{
|
|
$sql = <<<SQL
|
|
SELECT distinct(stream_id)
|
|
FROM cc_schedule
|
|
WHERE ends > now() AT TIME ZONE 'UTC'
|
|
AND stream_id is not null
|
|
SQL;
|
|
$streams = Application_Common_Database::prepareAndExecute( $sql, array());
|
|
|
|
$real_streams = array();
|
|
foreach ($streams as $s) {
|
|
$real_streams[] = $s['stream_id'];
|
|
}
|
|
|
|
return $real_streams;
|
|
}
|
|
/**
|
|
* Returns data related to the scheduled items.
|
|
*
|
|
* @param int $p_prev
|
|
* @param int $p_next
|
|
* @return date
|
|
*/
|
|
public static function GetPlayOrderRange($p_prev = 1, $p_next = 1)
|
|
{
|
|
//Everything in this function must be done in UTC. You will get a swift kick in the pants if you mess that up.
|
|
|
|
if (!is_int($p_prev) || !is_int($p_next)) {
|
|
//must enter integers to specify ranges
|
|
Logging::info("Invalid range parameters: $p_prev or $p_next");
|
|
|
|
return array();
|
|
}
|
|
|
|
$utcNow = new DateTime("now", new DateTimeZone("UTC"));
|
|
|
|
$shows = Application_Model_Show::getPrevCurrentNext($utcNow);
|
|
$previousShowID = count($shows['previousShow'])>0?$shows['previousShow'][0]['instance_id']:null;
|
|
$currentShowID = count($shows['currentShow'])>0?$shows['currentShow'][0]['instance_id']:null;
|
|
$nextShowID = count($shows['nextShow'])>0?$shows['nextShow'][0]['instance_id']:null;
|
|
$results = self::GetPrevCurrentNext($previousShowID, $currentShowID, $nextShowID, $utcNow);
|
|
|
|
$range = array("env"=>APPLICATION_ENV,
|
|
"schedulerTime"=> $utcNow->format("Y-m-d H:i:s"),
|
|
//Previous, current, next songs!
|
|
"previous"=>$results['previous'] !=null?$results['previous']:(count($shows['previousShow'])>0?$shows['previousShow'][0]:null),
|
|
"current"=>$results['current'] !=null?$results['current']:((count($shows['currentShow'])>0 && $shows['currentShow'][0]['record'] == 1)?$shows['currentShow'][0]:null),
|
|
"next"=> $results['next'] !=null?$results['next']:(count($shows['nextShow'])>0?$shows['nextShow'][0]:null),
|
|
//Current and next shows
|
|
"currentShow"=>$shows['currentShow'],
|
|
"nextShow"=>$shows['nextShow'],
|
|
);
|
|
|
|
return $range;
|
|
}
|
|
|
|
/**
|
|
* Queries the database for the set of schedules one hour before
|
|
* and after the given time. If a show starts and ends within that
|
|
* time that is considered the current show. Then the scheduled item
|
|
* before it is the previous show, and the scheduled item after it
|
|
* is the next show. This way the dashboard getCurrentPlaylist is
|
|
* very fast. But if any one of the three show types are not found
|
|
* through this mechanism a call is made to the old way of querying
|
|
* the database to find the track info.
|
|
**/
|
|
public static function GetPrevCurrentNext($p_previousShowID, $p_currentShowID, $p_nextShowID, $utcNow)
|
|
{
|
|
$timeZone = new DateTimeZone("UTC"); //This function works entirely in UTC.
|
|
assert(get_class($utcNow) === "DateTime");
|
|
assert($utcNow->getTimeZone() == $timeZone);
|
|
|
|
if ($p_previousShowID == null && $p_currentShowID == null && $p_nextShowID == null) {
|
|
return;
|
|
}
|
|
|
|
$sql = "SELECT %%columns%% st.starts as starts, st.ends as ends,
|
|
st.media_item_played as media_item_played, si.ends as show_ends
|
|
%%tables%% WHERE ";
|
|
|
|
$fileColumns = "ft.artist_name, ft.track_title, ";
|
|
$fileJoin = "FROM cc_schedule st JOIN cc_files ft ON st.file_id = ft.id
|
|
LEFT JOIN cc_show_instances si ON st.instance_id = si.id";
|
|
|
|
$streamColumns = "ws.name AS artist_name, wm.liquidsoap_data AS track_title, ";
|
|
$streamJoin = <<<SQL
|
|
FROM cc_schedule AS st
|
|
JOIN cc_webstream ws ON st.stream_id = ws.id
|
|
LEFT JOIN cc_show_instances AS si ON st.instance_id = si.id
|
|
LEFT JOIN cc_subjs AS sub ON sub.id = ws.creator_id
|
|
LEFT JOIN
|
|
(SELECT *
|
|
FROM cc_webstream_metadata
|
|
ORDER BY start_time DESC LIMIT 1) AS wm ON st.id = wm.instance_id
|
|
SQL;
|
|
|
|
$predicateArr = array();
|
|
$paramMap = array();
|
|
if (isset($p_previousShowID)) {
|
|
$predicateArr[] = 'st.instance_id = :previousShowId';
|
|
$paramMap[':previousShowId'] = $p_previousShowID;
|
|
}
|
|
if (isset($p_currentShowID)) {
|
|
$predicateArr[] = 'st.instance_id = :currentShowId';
|
|
$paramMap[':currentShowId'] = $p_currentShowID;
|
|
}
|
|
if (isset($p_nextShowID)) {
|
|
$predicateArr[] = 'st.instance_id = :nextShowId';
|
|
$paramMap[':nextShowId'] = $p_nextShowID;
|
|
}
|
|
|
|
$sql .= " (".implode(" OR ", $predicateArr).") ";
|
|
$sql .= ' AND st.playout_status > 0 ORDER BY st.starts';
|
|
|
|
$filesSql = str_replace("%%columns%%", $fileColumns, $sql);
|
|
$filesSql = str_replace("%%tables%%", $fileJoin, $filesSql);
|
|
|
|
$streamSql = str_replace("%%columns%%", $streamColumns, $sql);
|
|
$streamSql = str_replace("%%tables%%", $streamJoin, $streamSql);
|
|
|
|
$sql = "SELECT * FROM (($filesSql) UNION ($streamSql)) AS unioned ORDER BY starts";
|
|
|
|
$rows = Application_Common_Database::prepareAndExecute($sql, $paramMap);
|
|
$numberOfRows = count($rows);
|
|
|
|
$results['previous'] = null;
|
|
$results['current'] = null;
|
|
$results['next'] = null;
|
|
|
|
for ($i = 0; $i < $numberOfRows; ++$i) {
|
|
|
|
// if the show is overbooked, then update the track end time to the end of the show time.
|
|
if ($rows[$i]['ends'] > $rows[$i]["show_ends"]) {
|
|
$rows[$i]['ends'] = $rows[$i]["show_ends"];
|
|
}
|
|
|
|
$curShowStartTime = new DateTime($rows[$i]['starts'], $timeZone);
|
|
$curShowEndTime = new DateTime($rows[$i]['ends'], $timeZone);
|
|
|
|
if (($curShowStartTime <= $utcNow) && ($curShowEndTime >= $utcNow)) {
|
|
if ($i - 1 >= 0) {
|
|
$results['previous'] = array("name"=>$rows[$i-1]["artist_name"]." - ".$rows[$i-1]["track_title"],
|
|
"starts"=>$rows[$i-1]["starts"],
|
|
"ends"=>$rows[$i-1]["ends"],
|
|
"type"=>'track');
|
|
}
|
|
$results['current'] = array("name"=>$rows[$i]["artist_name"]." - ".$rows[$i]["track_title"],
|
|
"starts"=>$rows[$i]["starts"],
|
|
"ends"=> (($rows[$i]["ends"] > $rows[$i]["show_ends"]) ? $rows[$i]["show_ends"]: $rows[$i]["ends"]),
|
|
"media_item_played"=>$rows[$i]["media_item_played"],
|
|
"record"=>0,
|
|
"type"=>'track');
|
|
if (isset($rows[$i+1])) {
|
|
$results['next'] = array("name"=>$rows[$i+1]["artist_name"]." - ".$rows[$i+1]["track_title"],
|
|
"starts"=>$rows[$i+1]["starts"],
|
|
"ends"=>$rows[$i+1]["ends"],
|
|
"type"=>'track');
|
|
}
|
|
break;
|
|
}
|
|
if ($curShowEndTime < $utcNow ) {
|
|
$previousIndex = $i;
|
|
}
|
|
if ($curShowStartTime > $utcNow) {
|
|
$results['next'] = array("name"=>$rows[$i]["artist_name"]." - ".$rows[$i]["track_title"],
|
|
"starts"=>$rows[$i]["starts"],
|
|
"ends"=>$rows[$i]["ends"],
|
|
"type"=>'track');
|
|
break;
|
|
}
|
|
}
|
|
//If we didn't find a a current show because the time didn't fit we may still have
|
|
//found a previous show so use it.
|
|
if ($results['previous'] === null && isset($previousIndex)) {
|
|
$results['previous'] = array("name"=>$rows[$previousIndex]["artist_name"]." - ".$rows[$previousIndex]["track_title"],
|
|
"starts"=>$rows[$previousIndex]["starts"],
|
|
"ends"=>$rows[$previousIndex]["ends"]);;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
public static function GetLastScheduleItem($p_timeNow)
|
|
{
|
|
$sql = <<<SQL
|
|
SELECT ft.artist_name,
|
|
ft.track_title,
|
|
st.starts AS starts,
|
|
st.ends AS ends
|
|
FROM cc_schedule st
|
|
LEFT JOIN cc_files ft ON st.file_id = ft.id
|
|
LEFT JOIN cc_show_instances sit ON st.instance_id = sit.id
|
|
-- this and the next line are necessary since we can overbook shows.
|
|
WHERE st.ends < TIMESTAMP :timeNow
|
|
|
|
AND st.starts >= sit.starts
|
|
AND st.starts < sit.ends
|
|
ORDER BY st.ends DESC LIMIT 1;
|
|
SQL;
|
|
$row = Application_Common_Database::prepareAndExecute($sql, array(':timeNow'=>$p_timeNow));
|
|
|
|
return $row;
|
|
}
|
|
|
|
public static function GetCurrentScheduleItem($p_timeNow, $p_instanceId)
|
|
{
|
|
/* Note that usually there will be one result returned. In some
|
|
* rare cases two songs are returned. This happens when a track
|
|
* that was overbooked from a previous show appears as if it
|
|
* hasnt ended yet (track end time hasn't been reached yet). For
|
|
* this reason, we need to get the track that starts later, as
|
|
* this is the *real* track that is currently playing. So this
|
|
* is why we are ordering by track start time. */
|
|
$sql = "SELECT *"
|
|
." FROM cc_schedule st"
|
|
." LEFT JOIN cc_files ft"
|
|
." ON st.file_id = ft.id"
|
|
." WHERE st.starts <= TIMESTAMP :timeNow1"
|
|
." AND st.instance_id = :instanceId"
|
|
." AND st.ends > TIMESTAMP :timeNow2"
|
|
." ORDER BY st.starts DESC"
|
|
." LIMIT 1";
|
|
|
|
$row = Application_Common_Database::prepareAndExecute($sql, array(':timeNow1'=>$p_timeNow, ':instanceId'=>$p_instanceId, ':timeNow2'=>$p_timeNow,));
|
|
|
|
return $row;
|
|
}
|
|
|
|
public static function GetNextScheduleItem($p_timeNow)
|
|
{
|
|
$sql = "SELECT"
|
|
." ft.artist_name, ft.track_title,"
|
|
." st.starts as starts, st.ends as ends"
|
|
." FROM cc_schedule st"
|
|
." LEFT JOIN cc_files ft"
|
|
." ON st.file_id = ft.id"
|
|
." LEFT JOIN cc_show_instances sit"
|
|
." ON st.instance_id = sit.id"
|
|
." WHERE st.starts > TIMESTAMP :timeNow"
|
|
." AND st.starts >= sit.starts" //this and the next line are necessary since we can overbook shows.
|
|
." AND st.starts < sit.ends"
|
|
." ORDER BY st.starts"
|
|
." LIMIT 1";
|
|
|
|
$row = Application_Common_Database::prepareAndExecute($sql, array(':timeNow'=>$p_timeNow));
|
|
|
|
return $row;
|
|
}
|
|
|
|
/*
|
|
*
|
|
* @param DateTime $p_startDateTime
|
|
*
|
|
* @param DateTime $p_endDateTime
|
|
*
|
|
* @return array $scheduledItems
|
|
*
|
|
*/
|
|
public static function GetScheduleDetailItems($p_start, $p_end, $p_shows, $p_show_instances)
|
|
{
|
|
$p_start_str = $p_start->format("Y-m-d H:i:s");
|
|
$p_end_str = $p_end->format("Y-m-d H:i:s");
|
|
|
|
//We need to search 48 hours before and after the show times so that that we
|
|
//capture all of the show's contents.
|
|
$p_track_start= $p_start->sub(new DateInterval("PT48H"))->format("Y-m-d H:i:s");
|
|
$p_track_end = $p_end->add(new DateInterval("PT48H"))->format("Y-m-d H:i:s");
|
|
|
|
$templateSql = <<<SQL
|
|
SELECT DISTINCT sched.starts AS sched_starts,
|
|
sched.ends AS sched_ends,
|
|
sched.id AS sched_id,
|
|
sched.cue_in AS cue_in,
|
|
sched.cue_out AS cue_out,
|
|
sched.fade_in AS fade_in,
|
|
sched.fade_out AS fade_out,
|
|
sched.playout_status AS playout_status,
|
|
sched.instance_id AS sched_instance_id,
|
|
|
|
%%columns%%
|
|
FROM (%%join%%)
|
|
SQL;
|
|
|
|
$filesColumns = <<<SQL
|
|
ft.track_title AS file_track_title,
|
|
ft.artist_name AS file_artist_name,
|
|
ft.album_title AS file_album_title,
|
|
ft.length AS file_length,
|
|
ft.file_exists AS file_exists,
|
|
ft.mime AS file_mime,
|
|
ft.soundcloud_id AS soundcloud_id
|
|
SQL;
|
|
$filesJoin = <<<SQL
|
|
cc_schedule AS sched
|
|
JOIN cc_files AS ft ON (sched.file_id = ft.id
|
|
AND ((sched.starts >= :fj_ts_1
|
|
AND sched.starts < :fj_ts_2)
|
|
OR (sched.ends > :fj_ts_3
|
|
AND sched.ends <= :fj_ts_4)
|
|
OR (sched.starts <= :fj_ts_5
|
|
AND sched.ends >= :fj_ts_6))
|
|
)
|
|
SQL;
|
|
$paramMap = array(
|
|
":fj_ts_1" => $p_track_start,
|
|
":fj_ts_2" => $p_track_end,
|
|
":fj_ts_3" => $p_track_start,
|
|
":fj_ts_4" => $p_track_end,
|
|
":fj_ts_5" => $p_track_start,
|
|
":fj_ts_6" => $p_track_end,
|
|
);
|
|
|
|
$filesSql = str_replace("%%columns%%",
|
|
$filesColumns,
|
|
$templateSql);
|
|
$filesSql= str_replace("%%join%%",
|
|
$filesJoin,
|
|
$filesSql);
|
|
|
|
$streamColumns = <<<SQL
|
|
ws.name AS file_track_title,
|
|
sub.login AS file_artist_name,
|
|
ws.description AS file_album_title,
|
|
ws.length AS file_length,
|
|
't'::BOOL AS file_exists,
|
|
ws.mime AS file_mime,
|
|
(SELECT NULL::integer AS soundcloud_id)
|
|
SQL;
|
|
$streamJoin = <<<SQL
|
|
cc_schedule AS sched
|
|
JOIN cc_webstream AS ws ON (sched.stream_id = ws.id
|
|
AND ((sched.starts >= :sj_ts_1
|
|
AND sched.starts < :sj_ts_2)
|
|
OR (sched.ends > :sj_ts_3
|
|
AND sched.ends <= :sj_ts_4)
|
|
OR (sched.starts <= :sj_ts_5
|
|
AND sched.ends >= :sj_ts_6))
|
|
)
|
|
LEFT JOIN cc_subjs AS sub ON (ws.creator_id = sub.id)
|
|
SQL;
|
|
$map = array(
|
|
":sj_ts_1" => $p_track_start,
|
|
":sj_ts_2" => $p_track_end,
|
|
":sj_ts_3" => $p_track_start,
|
|
":sj_ts_4" => $p_track_end,
|
|
":sj_ts_5" => $p_track_start,
|
|
":sj_ts_6" => $p_track_end,
|
|
);
|
|
$paramMap = $paramMap + $map;
|
|
|
|
$streamSql = str_replace("%%columns%%",
|
|
$streamColumns,
|
|
$templateSql);
|
|
$streamSql = str_replace("%%join%%",
|
|
$streamJoin,
|
|
$streamSql);
|
|
|
|
|
|
$showPredicate = "";
|
|
if (count($p_shows) > 0) {
|
|
|
|
$params = array();
|
|
$map = array();
|
|
|
|
for ($i = 0, $len = count($p_shows); $i < $len; $i++) {
|
|
$holder = ":show_".$i;
|
|
|
|
$params[] = $holder;
|
|
$map[$holder] = $p_shows[$i];
|
|
}
|
|
|
|
$showPredicate = " AND show_id IN (".implode(",", $params).")";
|
|
$paramMap = $paramMap + $map;
|
|
} else if (count($p_show_instances) > 0) {
|
|
$showPredicate = " AND si.id IN (".implode(",", $p_show_instances).")";
|
|
}
|
|
|
|
$sql = <<<SQL
|
|
SELECT showt.name AS show_name,
|
|
showt.color AS show_color,
|
|
showt.background_color AS show_background_color,
|
|
showt.id AS show_id,
|
|
showt.linked AS linked,
|
|
si.starts AS si_starts,
|
|
si.ends AS si_ends,
|
|
si.time_filled AS si_time_filled,
|
|
si.record AS si_record,
|
|
si.rebroadcast AS si_rebroadcast,
|
|
si.instance_id AS parent_show,
|
|
si.id AS si_id,
|
|
si.last_scheduled AS si_last_scheduled,
|
|
si.file_id AS si_file_id,
|
|
*
|
|
FROM (($filesSql) UNION ($streamSql)) as temp
|
|
RIGHT JOIN cc_show_instances AS si ON (si.id = sched_instance_id)
|
|
JOIN cc_show AS showt ON (showt.id = si.show_id)
|
|
WHERE si.modified_instance = FALSE
|
|
$showPredicate
|
|
AND ((si.starts >= :ts_1
|
|
AND si.starts < :ts_2)
|
|
OR (si.ends > :ts_3
|
|
AND si.ends <= :ts_4)
|
|
OR (si.starts <= :ts_5
|
|
AND si.ends >= :ts_6))
|
|
ORDER BY si_starts,
|
|
sched_starts;
|
|
SQL;
|
|
|
|
$map = array(
|
|
":ts_1" => $p_start_str,
|
|
":ts_2" => $p_end_str,
|
|
":ts_3" => $p_start_str,
|
|
":ts_4" => $p_end_str,
|
|
":ts_5" => $p_start_str,
|
|
":ts_6" => $p_end_str,
|
|
);
|
|
$paramMap = $paramMap + $map;
|
|
|
|
$rows = Application_Common_Database::prepareAndExecute(
|
|
$sql,
|
|
$paramMap,
|
|
Application_Common_Database::ALL
|
|
);
|
|
|
|
return $rows;
|
|
}
|
|
|
|
public static function UpdateMediaPlayedStatus($p_id)
|
|
{
|
|
$sql = "UPDATE cc_schedule"
|
|
." SET media_item_played=TRUE";
|
|
// we need to update 'broadcasted' column as well
|
|
// check the current switch status
|
|
$live_dj = Application_Model_Preference::GetSourceSwitchStatus('live_dj') == 'on';
|
|
$master_dj = Application_Model_Preference::GetSourceSwitchStatus('master_dj') == 'on';
|
|
$scheduled_play = Application_Model_Preference::GetSourceSwitchStatus('scheduled_play') == 'on';
|
|
|
|
if (!$live_dj && !$master_dj && $scheduled_play) {
|
|
$sql .= ", broadcasted=1";
|
|
}
|
|
|
|
$sql .= " WHERE id=:pid";
|
|
$map = array(":pid" => $p_id);
|
|
|
|
Application_Common_Database::prepareAndExecute($sql, $map,
|
|
Application_Common_Database::EXECUTE);
|
|
}
|
|
|
|
public static function UpdateBrodcastedStatus($dateTime, $value)
|
|
{
|
|
$now = $dateTime->format("Y-m-d H:i:s");
|
|
|
|
$sql = <<<SQL
|
|
UPDATE cc_schedule
|
|
SET broadcasted=:broadcastedValue
|
|
WHERE starts <= :starts::TIMESTAMP
|
|
AND ends >= :ends::TIMESTAMP
|
|
SQL;
|
|
|
|
$retVal = Application_Common_Database::prepareAndExecute($sql, array(
|
|
':broadcastedValue' => $value,
|
|
':starts' => $now,
|
|
':ends' => $now), 'execute');
|
|
return $retVal;
|
|
}
|
|
|
|
public static function getSchduledPlaylistCount()
|
|
{
|
|
$sql = "SELECT count(*) as cnt FROM cc_schedule";
|
|
|
|
$res = Application_Common_Database::prepareAndExecute($sql, array(),
|
|
Application_Common_Database::COLUMN);
|
|
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Convert a time string in the format "YYYY-MM-DD HH:mm:SS"
|
|
* to "YYYY-MM-DD-HH-mm-SS".
|
|
*
|
|
* @param string $p_time
|
|
* @return string
|
|
*/
|
|
private static function AirtimeTimeToPypoTime($p_time)
|
|
{
|
|
$p_time = substr($p_time, 0, 19);
|
|
$p_time = str_replace(" ", "-", $p_time);
|
|
$p_time = str_replace(":", "-", $p_time);
|
|
|
|
return $p_time;
|
|
}
|
|
|
|
/**
|
|
* Convert a time string in the format "YYYY-MM-DD-HH-mm-SS" to
|
|
* "YYYY-MM-DD HH:mm:SS".
|
|
*
|
|
* @param string $p_time
|
|
* @return string
|
|
*/
|
|
private static function PypoTimeToAirtimeTime($p_time)
|
|
{
|
|
$t = explode("-", $p_time);
|
|
|
|
return $t[0]."-".$t[1]."-".$t[2]." ".$t[3].":".$t[4].":00";
|
|
}
|
|
|
|
/**
|
|
* Return true if the input string is in the format YYYY-MM-DD-HH-mm
|
|
*
|
|
* @param string $p_time
|
|
* @return boolean
|
|
*/
|
|
public static function ValidPypoTimeFormat($p_time)
|
|
{
|
|
$t = explode("-", $p_time);
|
|
if (count($t) != 5) {
|
|
return false;
|
|
}
|
|
foreach ($t as $part) {
|
|
if (!is_numeric($part)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Converts a time value as a string (with format HH:MM:SS.mmmmmm) to
|
|
* millisecs.
|
|
*
|
|
* @param string $p_time
|
|
* @return int
|
|
*/
|
|
public static function WallTimeToMillisecs($p_time)
|
|
{
|
|
$t = explode(":", $p_time);
|
|
$millisecs = 0;
|
|
if (strpos($t[2], ".")) {
|
|
$secParts = explode(".", $t[2]);
|
|
$millisecs = $secParts[1];
|
|
$millisecs = str_pad(substr($millisecs, 0, 3),3, '0');
|
|
$millisecs = intval($millisecs);
|
|
$seconds = intval($secParts[0]);
|
|
} else {
|
|
$seconds = intval($t[2]);
|
|
}
|
|
$ret = $millisecs + ($seconds * 1000) + ($t[1] * 60 * 1000) + ($t[0] * 60 * 60 * 1000);
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of schedule items from cc_schedule table. Tries
|
|
* to return at least 3 items (if they are available). The parameters
|
|
* $p_startTime and $p_endTime specify the range. Schedule items returned
|
|
* do not have to be entirely within this range. It is enough that the end
|
|
* or beginning of the scheduled item is in the range.
|
|
*
|
|
*
|
|
* @param string $p_startTime
|
|
* In the format YYYY-MM-DD HH:MM:SS.nnnnnn
|
|
* @param string $p_endTime
|
|
* In the format YYYY-MM-DD HH:MM:SS.nnnnnn
|
|
* @return array
|
|
* Returns null if nothing found, else an array of associative
|
|
* arrays representing each row.
|
|
*/
|
|
public static function getItems($p_startTime, $p_endTime)
|
|
{
|
|
$baseQuery = <<<SQL
|
|
SELECT st.file_id AS file_id,
|
|
st.id AS id,
|
|
st.instance_id AS instance_id,
|
|
st.starts AS start,
|
|
st.ends AS end,
|
|
st.cue_in AS cue_in,
|
|
st.cue_out AS cue_out,
|
|
st.fade_in AS fade_in,
|
|
st.fade_out AS fade_out,
|
|
si.starts AS show_start,
|
|
si.ends AS show_end,
|
|
s.name AS show_name,
|
|
f.id AS file_id,
|
|
f.replay_gain AS replay_gain,
|
|
ws.id AS stream_id,
|
|
ws.url AS url
|
|
FROM cc_schedule AS st
|
|
LEFT JOIN cc_show_instances AS si ON st.instance_id = si.id
|
|
LEFT JOIN cc_show AS s ON s.id = si.show_id
|
|
LEFT JOIN cc_files AS f ON st.file_id = f.id
|
|
LEFT JOIN cc_webstream AS ws ON st.stream_id = ws.id
|
|
SQL;
|
|
$predicates = <<<SQL
|
|
WHERE st.ends > :startTime1
|
|
AND st.starts < :endTime
|
|
AND st.playout_status > 0
|
|
AND si.ends > :startTime2
|
|
AND si.modified_instance = 'f'
|
|
ORDER BY st.starts
|
|
SQL;
|
|
|
|
$sql = $baseQuery." ".$predicates;
|
|
|
|
$rows = Application_Common_Database::prepareAndExecute($sql, array(
|
|
':startTime1' => $p_startTime,
|
|
':endTime' => $p_endTime,
|
|
':startTime2' => $p_startTime));
|
|
|
|
if (count($rows) < 3) {
|
|
$dt = new DateTime("@".time());
|
|
$dt->add(new DateInterval("PT24H"));
|
|
$range_end = $dt->format("Y-m-d H:i:s");
|
|
|
|
$predicates = <<<SQL
|
|
WHERE st.ends > :startTime1
|
|
AND st.starts < :rangeEnd
|
|
AND st.playout_status > 0
|
|
AND si.ends > :startTime2
|
|
AND si.modified_instance = 'f'
|
|
ORDER BY st.starts LIMIT 3
|
|
SQL;
|
|
|
|
$sql = $baseQuery." ".$predicates." ";
|
|
$rows = Application_Common_Database::prepareAndExecute($sql,
|
|
array(
|
|
':startTime1' => $p_startTime,
|
|
':rangeEnd' => $range_end,
|
|
':startTime2' => $p_startTime));
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* This function will ensure that an existing index in the
|
|
* associative array is never overwritten, instead appending
|
|
* _0, _1, _2, ... to the end of the key to make sure it is unique
|
|
*/
|
|
private static function appendScheduleItem(&$data, $time, $item)
|
|
{
|
|
$key = $time;
|
|
$i = 0;
|
|
|
|
while (array_key_exists($key, $data["media"])) {
|
|
$key = "{$time}_{$i}";
|
|
$i++;
|
|
}
|
|
|
|
$data["media"][$key] = $item;
|
|
}
|
|
|
|
private static function createInputHarborKickTimes(&$data, $range_start, $range_end)
|
|
{
|
|
$utcTimeZone = new DateTimeZone("UTC");
|
|
$kick_times = Application_Model_ShowInstance::GetEndTimeOfNextShowWithLiveDJ($range_start, $range_end);
|
|
foreach ($kick_times as $kick_time_info) {
|
|
$kick_time = $kick_time_info['ends'];
|
|
$temp = explode('.', Application_Model_Preference::GetDefaultTransitionFade());
|
|
// we round down transition time since PHP cannot handle millisecond. We need to
|
|
// handle this better in the future
|
|
$transition_time = intval($temp[0]);
|
|
$switchOffDataTime = new DateTime($kick_time, $utcTimeZone);
|
|
$switch_off_time = $switchOffDataTime->sub(new DateInterval('PT'.$transition_time.'S'));
|
|
$switch_off_time = $switch_off_time->format("Y-m-d H:i:s");
|
|
|
|
$kick_start = self::AirtimeTimeToPypoTime($kick_time);
|
|
$data["media"][$kick_start]['start'] = $kick_start;
|
|
$data["media"][$kick_start]['end'] = $kick_start;
|
|
$data["media"][$kick_start]['event_type'] = "kick_out";
|
|
$data["media"][$kick_start]['type'] = "event";
|
|
$data["media"][$kick_start]['independent_event'] = true;
|
|
|
|
if ($kick_time !== $switch_off_time) {
|
|
$switch_start = self::AirtimeTimeToPypoTime($switch_off_time);
|
|
$data["media"][$switch_start]['start'] = $switch_start;
|
|
$data["media"][$switch_start]['end'] = $switch_start;
|
|
$data["media"][$switch_start]['event_type'] = "switch_off";
|
|
$data["media"][$switch_start]['type'] = "event";
|
|
$data["media"][$switch_start]['independent_event'] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function createFileScheduleEvent(&$data, $item, $media_id, $uri)
|
|
{
|
|
$start = self::AirtimeTimeToPypoTime($item["start"]);
|
|
$end = self::AirtimeTimeToPypoTime($item["end"]);
|
|
|
|
list(,,,$start_hour,,) = explode("-", $start);
|
|
list(,,,$end_hour,,) = explode("-", $end);
|
|
|
|
$same_hour = $start_hour == $end_hour;
|
|
$independent_event = !$same_hour;
|
|
|
|
$replay_gain = is_null($item["replay_gain"]) ? "0": $item["replay_gain"];
|
|
$replay_gain += Application_Model_Preference::getReplayGainModifier();
|
|
|
|
if ( !Application_Model_Preference::GetEnableReplayGain() ) {
|
|
$replay_gain = 0;
|
|
}
|
|
|
|
$schedule_item = array(
|
|
'id' => $media_id,
|
|
'type' => 'file',
|
|
'row_id' => $item["id"],
|
|
'uri' => $uri,
|
|
'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]),
|
|
'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]),
|
|
'cue_in' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_in"]),
|
|
'cue_out' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_out"]),
|
|
'start' => $start,
|
|
'end' => $end,
|
|
'show_name' => $item["show_name"],
|
|
'replay_gain' => $replay_gain,
|
|
'independent_event' => $independent_event,
|
|
);
|
|
|
|
if ($schedule_item['cue_in'] > $schedule_item['cue_out']) {
|
|
$schedule_item['cue_in'] = $schedule_item['cue_out'];
|
|
}
|
|
self::appendScheduleItem($data, $start, $schedule_item);
|
|
}
|
|
|
|
private static function createStreamScheduleEvent(&$data, $item, $media_id, $uri)
|
|
{
|
|
$start = self::AirtimeTimeToPypoTime($item["start"]);
|
|
$end = self::AirtimeTimeToPypoTime($item["end"]);
|
|
|
|
//create an event to start stream buffering 5 seconds ahead of the streams actual time.
|
|
$buffer_start = new DateTime($item["start"], new DateTimeZone('UTC'));
|
|
$buffer_start->sub(new DateInterval("PT5S"));
|
|
|
|
$stream_buffer_start = self::AirtimeTimeToPypoTime($buffer_start->format("Y-m-d H:i:s"));
|
|
|
|
$schedule_item = array(
|
|
'start' => $stream_buffer_start,
|
|
'end' => $stream_buffer_start,
|
|
'uri' => $uri,
|
|
'row_id' => $item["id"],
|
|
'type' => 'stream_buffer_start',
|
|
'independent_event' => true
|
|
);
|
|
|
|
self::appendScheduleItem($data, $start, $schedule_item);
|
|
|
|
$schedule_item = array(
|
|
'id' => $media_id,
|
|
'type' => 'stream_output_start',
|
|
'row_id' => $item["id"],
|
|
'uri' => $uri,
|
|
'start' => $start,
|
|
'end' => $end,
|
|
'show_name' => $item["show_name"],
|
|
'independent_event' => true
|
|
);
|
|
self::appendScheduleItem($data, $start, $schedule_item);
|
|
|
|
//since a stream never ends we have to insert an additional "kick stream" event. The "start"
|
|
//time of this event is the "end" time of the stream minus 1 second.
|
|
$dt = new DateTime($item["end"], new DateTimeZone('UTC'));
|
|
$dt->sub(new DateInterval("PT1S"));
|
|
|
|
$stream_end = self::AirtimeTimeToPypoTime($dt->format("Y-m-d H:i:s"));
|
|
|
|
$schedule_item = array(
|
|
'start' => $stream_end,
|
|
'end' => $stream_end,
|
|
'uri' => $uri,
|
|
'type' => 'stream_buffer_end',
|
|
'row_id' => $item["id"],
|
|
'independent_event' => true
|
|
);
|
|
self::appendScheduleItem($data, $stream_end, $schedule_item);
|
|
|
|
$schedule_item = array(
|
|
'start' => $stream_end,
|
|
'end' => $stream_end,
|
|
'uri' => $uri,
|
|
'type' => 'stream_output_end',
|
|
'independent_event' => true
|
|
);
|
|
self::appendScheduleItem($data, $stream_end, $schedule_item);
|
|
}
|
|
|
|
private static function getRangeStartAndEnd($p_fromDateTime, $p_toDateTime)
|
|
{
|
|
$CC_CONFIG = Config::getConfig();
|
|
|
|
$utcTimeZone = new DateTimeZone('UTC');
|
|
|
|
/* if $p_fromDateTime and $p_toDateTime function parameters are null,
|
|
then set range * from "now" to "now + 24 hours". */
|
|
if (is_null($p_fromDateTime)) {
|
|
$t1 = new DateTime("@".time(), $utcTimeZone);
|
|
$range_start = $t1->format("Y-m-d H:i:s");
|
|
} else {
|
|
$range_start = Application_Model_Schedule::PypoTimeToAirtimeTime($p_fromDateTime);
|
|
}
|
|
if (is_null($p_fromDateTime)) {
|
|
$t2 = new DateTime("@".time(), $utcTimeZone);
|
|
|
|
$cache_ahead_hours = $CC_CONFIG["cache_ahead_hours"];
|
|
|
|
if (is_numeric($cache_ahead_hours)) {
|
|
//make sure we are not dealing with a float
|
|
$cache_ahead_hours = intval($cache_ahead_hours);
|
|
} else {
|
|
$cache_ahead_hours = 1;
|
|
}
|
|
|
|
$t2->add(new DateInterval("PT".$cache_ahead_hours."H"));
|
|
$range_end = $t2->format("Y-m-d H:i:s");
|
|
} else {
|
|
$range_end = Application_Model_Schedule::PypoTimeToAirtimeTime($p_toDateTime);
|
|
}
|
|
|
|
return array($range_start, $range_end);
|
|
}
|
|
|
|
|
|
private static function createScheduledEvents(&$data, $range_start, $range_end)
|
|
{
|
|
$utcTimeZone = new DateTimeZone("UTC");
|
|
$items = self::getItems($range_start, $range_end);
|
|
|
|
foreach ($items as $item) {
|
|
$showEndDateTime = new DateTime($item["show_end"], $utcTimeZone);
|
|
|
|
$trackStartDateTime = new DateTime($item["start"], $utcTimeZone);
|
|
$trackEndDateTime = new DateTime($item["end"], $utcTimeZone);
|
|
|
|
if ($trackStartDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) {
|
|
//do not send any tracks that start past their show's end time
|
|
continue;
|
|
}
|
|
|
|
if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) {
|
|
$di = $trackStartDateTime->diff($showEndDateTime);
|
|
|
|
$item["cue_out"] = $di->format("%H:%i:%s").".000";
|
|
$item["end"] = $showEndDateTime->format("Y-m-d H:i:s");
|
|
}
|
|
|
|
if (!is_null($item['file_id'])) {
|
|
//row is from "file"
|
|
$media_id = $item['file_id'];
|
|
$storedFile = Application_Model_StoredFile::RecallById($media_id);
|
|
$uri = $storedFile->getFilePath();
|
|
self::createFileScheduleEvent($data, $item, $media_id, $uri);
|
|
}
|
|
elseif (!is_null($item['stream_id'])) {
|
|
//row is type "webstream"
|
|
$media_id = $item['stream_id'];
|
|
$uri = $item['url'];
|
|
self::createStreamScheduleEvent($data, $item, $media_id, $uri);
|
|
}
|
|
else {
|
|
throw new Exception("Unknown schedule type: ".print_r($item, true));
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/* Check if two events are less than or equal to 1 second apart
|
|
*/
|
|
public static function areEventsLinked($event1, $event2) {
|
|
$dt1 = DateTime::createFromFormat("Y-m-d-H-i-s", $event1['start']);
|
|
$dt2 = DateTime::createFromFormat("Y-m-d-H-i-s", $event2['start']);
|
|
|
|
$seconds = $dt2->getTimestamp() - $dt1->getTimestamp();
|
|
return $seconds <= 1;
|
|
}
|
|
|
|
/**
|
|
* Streams are a 4 stage process.
|
|
* 1) start buffering stream 5 seconds ahead of its start time
|
|
* 2) at the start time tell liquidsoap to switch to this source
|
|
* 3) at the end time, tell liquidsoap to stop reading this stream
|
|
* 4) at the end time, tell liquidsoap to switch away from input.http source.
|
|
*
|
|
* When we have two streams back-to-back, some of these steps are unnecessary
|
|
* for the second stream. Instead of sending commands 1,2,3,4,1,2,3,4 we should
|
|
* send 1,2,1,2,3,4 - We don't need to tell liquidsoap to stop reading (#3), because #1
|
|
* of the next stream implies this when we pass in a new url. We also don't need #4.
|
|
*
|
|
* There's a special case here is well. When the back-to-back streams are the same, we
|
|
* can collapse the instructions 1,2,(3,4,1,2),3,4 to 1,2,3,4. We basically cut out the
|
|
* middle part. This function handles this.
|
|
*/
|
|
private static function foldData(&$data)
|
|
{
|
|
$previous_key = null;
|
|
$previous_val = null;
|
|
$previous_previous_key = null;
|
|
$previous_previous_val = null;
|
|
$previous_previous_previous_key = null;
|
|
$previous_previous_previous_val = null;
|
|
foreach ($data as $k => $v) {
|
|
|
|
if ($v["type"] == "stream_output_start"
|
|
&& !is_null($previous_previous_val)
|
|
&& $previous_previous_val["type"] == "stream_output_end"
|
|
&& self::areEventsLinked($previous_previous_val, $v)) {
|
|
|
|
unset($data[$previous_previous_previous_key]);
|
|
unset($data[$previous_previous_key]);
|
|
unset($data[$previous_key]);
|
|
if ($previous_previous_val['uri'] == $v['uri']) {
|
|
unset($data[$k]);
|
|
}
|
|
}
|
|
|
|
$previous_previous_previous_key = $previous_previous_key;
|
|
$previous_previous_previous_val = $previous_previous_val;
|
|
$previous_previous_key = $previous_key;
|
|
$previous_previous_val = $previous_val;
|
|
$previous_key = $k;
|
|
$previous_val = $v;
|
|
}
|
|
}
|
|
|
|
public static function getSchedule($p_fromDateTime = null, $p_toDateTime = null)
|
|
{
|
|
//generate repeating shows if we are fetching the schedule
|
|
//for days beyond the shows_populated_until value in cc_pref
|
|
$needScheduleUntil = $p_toDateTime;
|
|
if (is_null($needScheduleUntil)) {
|
|
$needScheduleUntil = new DateTime("now", new DateTimeZone("UTC"));
|
|
$needScheduleUntil->add(new DateInterval("P1D"));
|
|
}
|
|
$showsPopUntil = Application_Model_Preference::GetShowsPopulatedUntil();
|
|
//if application is requesting shows past our previous populated until date, generate shows up until this point.
|
|
if (is_null($showsPopUntil) || $showsPopUntil->getTimestamp() < $needScheduleUntil->getTimestamp()) {
|
|
$service_show = new Application_Service_ShowService();
|
|
$ccShow = $service_show->delegateInstanceCreation(null, $needScheduleUntil, true);
|
|
Application_Model_Preference::SetShowsPopulatedUntil($needScheduleUntil);
|
|
}
|
|
|
|
list($range_start, $range_end) = self::getRangeStartAndEnd($p_fromDateTime, $p_toDateTime);
|
|
|
|
$data = array();
|
|
$data["media"] = array();
|
|
|
|
//Harbor kick times *MUST* be ahead of schedule events, so that pypo
|
|
//executes them first.
|
|
self::createInputHarborKickTimes($data, $range_start, $range_end);
|
|
self::createScheduledEvents($data, $range_start, $range_end);
|
|
|
|
//self::foldData($data["media"]);
|
|
return $data;
|
|
}
|
|
|
|
public static function deleteAll()
|
|
{
|
|
$sql = "TRUNCATE TABLE cc_schedule";
|
|
Application_Common_Database::prepareAndExecute($sql, array(),
|
|
Application_Common_Database::EXECUTE);
|
|
}
|
|
|
|
public static function deleteWithFileId($fileId)
|
|
{
|
|
$sql = "DELETE FROM cc_schedule WHERE file_id=:file_id";
|
|
Application_Common_Database::prepareAndExecute($sql, array(':file_id'=>$fileId), 'execute');
|
|
}
|
|
|
|
public static function checkOverlappingShows($show_start, $show_end,
|
|
$update=false, $instanceId=null, $showId=null)
|
|
{
|
|
//if the show instance does not exist or was deleted, return false
|
|
if (!is_null($showId)) {
|
|
$ccShowInstance = CcShowInstancesQuery::create()
|
|
->filterByDbShowId($showId)
|
|
->filterByDbStarts($show_start->format("Y-m-d H:i:s"))
|
|
->findOne();
|
|
} elseif (!is_null($instanceId)) {
|
|
$ccShowInstance = CcShowInstancesQuery::create()
|
|
->filterByDbId($instanceId)
|
|
->findOne();
|
|
}
|
|
if ($update && ($ccShowInstance && $ccShowInstance->getDbModifiedInstance() == true)) {
|
|
return false;
|
|
}
|
|
|
|
$overlapping = false;
|
|
|
|
$params = array(
|
|
':show_end1' => $show_end->format('Y-m-d H:i:s'),
|
|
':show_end2' => $show_end->format('Y-m-d H:i:s'),
|
|
':show_end3' => $show_end->format('Y-m-d H:i:s')
|
|
);
|
|
|
|
|
|
/* If a show is being edited, exclude it from the query
|
|
* In both cases (new and edit) we only grab shows that
|
|
* are scheduled 2 days prior
|
|
*/
|
|
if ($update) {
|
|
$sql = <<<SQL
|
|
SELECT id,
|
|
starts,
|
|
ends
|
|
FROM cc_show_instances
|
|
WHERE (ends <= :show_end1
|
|
OR starts <= :show_end2)
|
|
AND date(starts) >= (date(:show_end3) - INTERVAL '2 days')
|
|
AND modified_instance = FALSE
|
|
SQL;
|
|
if (is_null($showId)) {
|
|
$sql .= <<<SQL
|
|
AND id != :instanceId
|
|
ORDER BY ends
|
|
SQL;
|
|
$params[':instanceId'] = $instanceId;
|
|
} else {
|
|
$sql .= <<<SQL
|
|
AND show_id != :showId
|
|
ORDER BY ends
|
|
SQL;
|
|
$params[':showId'] = $showId;
|
|
}
|
|
$rows = Application_Common_Database::prepareAndExecute($sql, $params, 'all');
|
|
} else {
|
|
$sql = <<<SQL
|
|
SELECT id,
|
|
starts,
|
|
ends
|
|
FROM cc_show_instances
|
|
WHERE (ends <= :show_end1
|
|
OR starts <= :show_end2)
|
|
AND date(starts) >= (date(:show_end3) - INTERVAL '2 days')
|
|
AND modified_instance = FALSE
|
|
ORDER BY ends
|
|
SQL;
|
|
|
|
$rows = Application_Common_Database::prepareAndExecute($sql, array(
|
|
':show_end1' => $show_end->format('Y-m-d H:i:s'),
|
|
':show_end2' => $show_end->format('Y-m-d H:i:s'),
|
|
':show_end3' => $show_end->format('Y-m-d H:i:s')), 'all');
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
$start = new DateTime($row["starts"], new DateTimeZone('UTC'));
|
|
$end = new DateTime($row["ends"], new DateTimeZone('UTC'));
|
|
|
|
if ($show_start->getTimestamp() < $end->getTimestamp() &&
|
|
$show_end->getTimestamp() > $start->getTimestamp()) {
|
|
$overlapping = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $overlapping;
|
|
}
|
|
|
|
public static function GetType($p_scheduleId){
|
|
$scheduledItem = CcScheduleQuery::create()->findPK($p_scheduleId);
|
|
if ($scheduledItem->getDbFileId() == null) {
|
|
return 'webstream';
|
|
} else {
|
|
return 'file';
|
|
}
|
|
}
|
|
|
|
public static function GetFileId($p_scheduleId)
|
|
{
|
|
$scheduledItem = CcScheduleQuery::create()->findPK($p_scheduleId);
|
|
|
|
return $scheduledItem->getDbFileId();
|
|
}
|
|
|
|
public static function GetStreamId($p_scheduleId)
|
|
{
|
|
$scheduledItem = CcScheduleQuery::create()->findPK($p_scheduleId);
|
|
|
|
return $scheduledItem->getDbStreamId();
|
|
}
|
|
}
|