libretime/airtime_mvc/application/models/Schedule.php

588 lines
17 KiB
PHP

<?php
use Airtime\CcScheduleQuery;
use Airtime\CcShowInstancesQuery;
class Application_Model_Schedule
{
/*
*
* @param DateTime $start in UTC timezone
* @param DateTime $end in UTC timezone
*
* @return array $scheduledItems
*
*/
public static function GetScheduleDetailItems($start, $end, $getOnlyPlayable = false,
$showIds = array(), $showInstanceIds = array())
{
$utcNow = new DateTime("now", new DateTimeZone("UTC"));
//ordering first by show instance start time
//and then by scheduled item start time.
$items = CcShowInstancesQuery::create()
->filterByDbModifiedInstance(false)
->_if(isset($showIds) && count($showIds) > 0)
->filterByDbShowId($showIds)
->_endif()
->_if(isset($showInstanceIds) && count($showInstanceIds) > 0)
->filterByDbId($showInstanceIds)
->_endif()
->between($start, $end)
->orderByDbStarts()
//including these relations to prevent further database queries for
->joinWith("CcShow", Criteria::LEFT_JOIN)
->useCcScheduleQuery(null, Criteria::LEFT_JOIN)
//only retrieve items that will get played in the future.
->_if($getOnlyPlayable)
->filterByDbPlayoutStatus(0, Criteria::GREATER_THAN)
->filterByDbEnds($utcNow->format("Y-m-d H:i:s"), Criteria::GREATER_THAN)
->_endif()
->orderByDbStarts()
->endUse()
->with("CcSchedule")
->joinWith("CcSchedule.MediaItem", Criteria::LEFT_JOIN)
->joinWith("MediaItem.AudioFile", Criteria::LEFT_JOIN)
->joinWith("MediaItem.Webstream", Criteria::LEFT_JOIN)
->find();
return $items;
}
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
*/
public 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;
}
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 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 + cache_ahead_hours". */
if (is_null($p_fromDateTime)) {
$p_fromDateTime = new DateTime("now", $utcTimeZone);
}
else {
$p_fromDateTime->setTimezone($utcTimeZone);
}
if (is_null($p_toDateTime)) {
$p_toDateTime = clone $p_fromDateTime;
$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;
}
$p_toDateTime->add(new DateInterval("PT".$cache_ahead_hours."H"));
}
else {
$p_toDateTime->setTimezone($utcTimeZone);
}
return array($p_fromDateTime, $p_toDateTime);
}
/*
* @param array $data output array for events, contains key "media"
* @param DateTime $startDT UTC start of schedule range
* @param DateTime $endDT UTC end of schedule range
*/
private static function createScheduledEvents(&$data, $startDT, $endDT)
{
$showInstances = self::GetScheduleDetailItems($startDT, $endDT, true);
//Logging::info($showInstances);
foreach ($showInstances as $showInstance) {
foreach($showInstance->getCcSchedules() as $scheduleItem) {
$event = $scheduleItem->createScheduleEvent($data);
}
}
}
public static function getSchedule($p_fromDateTime = null, $p_toDateTime = null)
{
//Logging::enablePropelLogging();
list($startDT, $endDT) = 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, $startDT->format("Y-m-d H:i:s"), $endDT->format("Y-m-d H:i:s"));
self::createScheduledEvents($data, $startDT, $endDT);
//Logging::disablePropelLogging();
return $data;
}
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;
}
private static function makeDashboardItemOutput(&$row)
{
if (empty($row["item_start"])) {
return null;
}
return array(
"name"=> $row["media_title"] ." - ".$row["media_creator"],
"starts" => $row["item_start"],
"ends" => (($row["item_end"] > $row["show_end"]) ? $row["show_end"]: $row["item_end"]),
"media_item_played" => (boolean) $row["media_item_played"],
"record" => 0,
"type" => 'track'
);
}
private static function makeDashboardShowOutput(&$row)
{
return array(
"id" => $row['show_id'],
"instance_id" => $row['instance_id'],
"name" => $row['show_name'],
"url" => $row['show_url'],
"start_timestamp" => $row['show_start'],
"end_timestamp" => $row['show_end'],
"starts" => $row['show_start'],
"ends" => $row['show_end'],
"record" => $row['is_recorded'],
"type" => "show"
);
}
public static function getDashboardInfo()
{
//TODO better to split this into 2 queries, 1 for shows, 1 for items.
$sql = <<<SQL
select
npItems.media_title,
npItems.media_creator,
npItems.item_start,
npItems.item_end,
npItems.show_start,
npItems.show_end,
npItems.show_id,
npItems.instance_id,
npItems.is_recorded,
npItems.media_item_played,
show.name as show_name,
show.url as show_url
from
(
select
pcnItems.media_title,
pcnItems.media_creator,
pcnItems.starts as item_start,
pcnItems.ends as item_end,
pcnItems.media_item_played,
pcnShows.starts as show_start,
pcnShows.ends as show_end,
pcnShows.show_id,
pcnShows.instance_id,
pcnShows.is_recorded
from
(
select
preCurrNextShows.starts,
preCurrNextShows.ends,
preCurrNextShows.show_id,
preCurrNextShows.id as instance_id,
preCurrNextShows.record as is_recorded
from
(
select * from
(
select * from cc_show_instances instance
where
instance.modified_instance = false
and instance.starts <= (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
and instance.ends > (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
)
as currInstance
union
select * from
(
select * from cc_show_instances instance
where
instance.modified_instance = false
and instance.starts > (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
order by instance.starts
limit 1
)
as nextInstance
union
select * from
(
select * from cc_show_instances instance
where
instance.modified_instance = false
and instance.ends < (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
order by instance.ends desc
limit 1
)
as prevInstance
)
as preCurrNextShows
)
as pcnShows
full outer join
(
select
preCurrNextItem.starts,
preCurrNextItem.ends,
preCurrNextItem.show_id,
preCurrNextItem.media_item_played,
media.name as media_title,
media.creator as media_creator
from
(
select * from
(select currentItem.starts, currentItem.ends, currentItem.media_id, currentItem.media_item_played, showInstance.show_id from
(select sched.starts, sched.ends, sched.instance_id, sched.media_id, sched.media_item_played from cc_schedule sched
where
sched.playout_status > 0
and sched.starts <= (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
and sched.ends > (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC'))
as currentItem
left join cc_show_instances showInstance on currentItem.instance_id = showInstance.id
where showInstance.modified_instance = false
and showInstance.starts <= (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
and showInstance.ends > (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC'))
as cItem
union
select * from
(select nextItem.starts, nextItem.ends, nextItem.media_id, nextItem.media_item_played, showInstance.show_id from
(select sched.starts, sched.ends, sched.instance_id, sched.media_id, sched.media_item_played from cc_schedule sched
where
sched.playout_status > 0
and sched.starts > (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
order by sched.starts
limit 1)
as nextItem
left join cc_show_instances showInstance on nextItem.instance_id = showInstance.id
where showInstance.modified_instance = false)
as nItem
union
select * from
(select prevItem.starts, prevItem.ends, prevItem.media_id, prevItem.media_item_played, showInstance.show_id from
(select sched.starts, sched.ends, sched.instance_id, sched.media_id, sched.media_item_played from cc_schedule sched
where
sched.playout_status > 0
and sched.ends < (select CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
order by sched.ends desc
limit 1)
as prevItem
left join cc_show_instances showInstance on prevItem.instance_id = showInstance.id
where showInstance.modified_instance = false)
as pItem
)
as preCurrNextItem
left join media_item media on preCurrNextItem.media_id = media.id
)
as pcnItems
using(show_id)
)
as npItems
left join cc_show show on npItems.show_id = show.id
where npItems.show_id is not null
order by
npItems.show_start,
npItems.item_start
SQL;
// extra rows can be created here from combining prev/curr/next items
// with prev/curr/next shows.
//this happens from recorded shows, or any kind of show that does not have a cc_schedule entry associated with it.
//at most 5 items will be returned, need to find the proper prev/curr/next.
$rows = Application_Common_Database::prepareAndExecute($sql);
//Logging::info($rows);
$prev = null;
$curr = null;
$next = null;
$utcTimezone = new DateTimeZone("UTC");
$utcNow = new DateTime("now", $utcTimezone);
for ($i = 0, $len = count($rows); $i < $len; $i++) {
$start = $rows[$i]["show_start"];
$end = $rows[$i]["show_end"];
$startDT = new DateTime($start, $utcTimezone);
$endDT = new DateTime($end, $utcTimezone);
if ($endDT < $utcNow) {
$prev = $rows[$i];
}
else if ($startDT <= $utcNow && $endDT > $utcNow) {
$curr = $rows[$i];
}
else {
$next = $rows[$i];
}
}
$prevShow = isset($prev) ? self::makeDashboardShowOutput($prev) : null;
$currShow = isset($curr) ? self::makeDashboardShowOutput($curr) : null;
$nextShow = isset($next) ? self::makeDashboardShowOutput($next) : null;
//start again to find items.
$prev = null;
$curr = null;
$next = null;
for ($i = 0, $len = count($rows); $i < $len; $i++) {
if (empty($rows[$i]["item_start"])) {
continue;
}
$start = $rows[$i]["item_start"];
$end = $rows[$i]["item_end"];
$startDT = new DateTime($start, $utcTimezone);
$endDT = new DateTime($end, $utcTimezone);
if ($endDT < $utcNow) {
$prev = $rows[$i];
}
else if ($startDT <= $utcNow && $endDT > $utcNow) {
$curr = $rows[$i];
//could theoretically have 2 currents with crossfades,
//need to update the previous here just incase.
//items are ordered by starts so we can assume this.
if ($i > 0) {
$prev = $rows[$i - 1];
}
}
else {
$next = $rows[$i];
//need to exit as extra rows can occur from future empty shows.
break;
}
}
$prevItem = isset($prev) ? self::makeDashboardItemOutput($prev) : null;
$currItem = isset($curr) ? self::makeDashboardItemOutput($curr) : null;
$nextItem = isset($next) ? self::makeDashboardItemOutput($next) : null;
$range = array("env"=>APPLICATION_ENV,
"schedulerTime"=> $utcNow->format("Y-m-d H:i:s"),
//Previous, current, next songs!
"previous"=> isset($prevItem) ? $prevItem : $prevShow,
//only pass back the current show as the current item if it's recording.
"current"=> isset($currItem) ? $currItem : (($currShow["record"] == 1) ? $currShow: null),
"next"=> isset($nextItem) ? $nextItem : $nextShow,
//Current and next shows
//TODO this is lame that they're sent back in an array instead of just an object.
//dashboard.js needs to be fixed up for this though.
"currentShow"=> isset($currShow) ? array($currShow) : array(),
"nextShow"=> isset($nextShow) ? array($nextShow) : array()
);
return $range;
}
}