Schedule.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. <?php
  2. class Application_Model_Schedule
  3. {
  4. /**
  5. * Return TRUE if file is going to be played in the future.
  6. *
  7. * @param string $p_fileId
  8. */
  9. public static function IsFileScheduledInTheFuture($p_fileId)
  10. {
  11. $sql = <<<SQL
  12. SELECT COUNT(*)
  13. FROM cc_schedule
  14. WHERE file_id = :file_id
  15. AND ends > NOW() AT TIME ZONE 'UTC'
  16. SQL;
  17. $count = Application_Common_Database::prepareAndExecute( $sql, array(
  18. ':file_id'=>$p_fileId), 'column');
  19. return (is_numeric($count) && ($count != '0'));
  20. }
  21. public static function getAllFutureScheduledFiles($instanceId=null)
  22. {
  23. $sql = <<<SQL
  24. SELECT distinct(file_id)
  25. FROM cc_schedule
  26. WHERE ends > now() AT TIME ZONE 'UTC'
  27. AND file_id is not null
  28. SQL;
  29. $files = Application_Common_Database::prepareAndExecute( $sql, array());
  30. $real_files = array();
  31. foreach ($files as $f) {
  32. $real_files[] = $f['file_id'];
  33. }
  34. return $real_files;
  35. }
  36. public static function getAllFutureScheduledWebstreams()
  37. {
  38. $sql = <<<SQL
  39. SELECT distinct(stream_id)
  40. FROM cc_schedule
  41. WHERE ends > now() AT TIME ZONE 'UTC'
  42. AND stream_id is not null
  43. SQL;
  44. $streams = Application_Common_Database::prepareAndExecute( $sql, array());
  45. $real_streams = array();
  46. foreach ($streams as $s) {
  47. $real_streams[] = $s['stream_id'];
  48. }
  49. return $real_streams;
  50. }
  51. /**
  52. * Returns data related to the scheduled items.
  53. */
  54. public static function GetPlayOrderRange($utcTimeEnd = null, $showsToRetrieve = 5)
  55. {
  56. // Everything in this function must be done in UTC. You will get a swift kick in the pants if you mess that up.
  57. // when timeEnd is unspecified, return to the default behaviour - set a range of 48 hours from current time
  58. if (!$utcTimeEnd) {
  59. $end = new DateTime();
  60. $end->add(new DateInterval("P2D")); // Add 2 days
  61. $end->setTimezone(new DateTimeZone("UTC"));
  62. $utcTimeEnd = $end->format("Y-m-d H:i:s");
  63. }
  64. $utcNow = new DateTime("now", new DateTimeZone("UTC"));
  65. $shows = Application_Model_Show::getPrevCurrentNext($utcNow, $utcTimeEnd, $showsToRetrieve);
  66. $previousShowID = count($shows['previousShow'])>0?$shows['previousShow'][0]['instance_id']:null;
  67. $currentShowID = count($shows['currentShow'])>0?$shows['currentShow']['instance_id']:null;
  68. $nextShowID = count($shows['nextShow'])>0?$shows['nextShow'][0]['instance_id']:null;
  69. $results = self::GetPrevCurrentNext($previousShowID, $currentShowID, $nextShowID, $utcNow);
  70. $range = array(
  71. "station" => array (
  72. "env" => APPLICATION_ENV,
  73. "schedulerTime" => $utcNow->format("Y-m-d H:i:s")
  74. ),
  75. //Previous, current, next songs!
  76. "tracks" => array(
  77. "previous" => $results['previous'],
  78. "current" => $results['current'],
  79. "next" => $results['next']
  80. ),
  81. //Current and next shows
  82. "shows" => array (
  83. "previous" => $shows['previousShow'],
  84. "current" => $shows['currentShow'],
  85. "next" => $shows['nextShow']
  86. )
  87. );
  88. return $range;
  89. }
  90. /**
  91. * Old version of the function for backwards compatibility
  92. * @deprecated
  93. */
  94. public static function GetPlayOrderRangeOld()
  95. {
  96. // Everything in this function must be done in UTC. You will get a swift kick in the pants if you mess that up.
  97. $utcNow = new DateTime("now", new DateTimeZone("UTC"));
  98. $shows = Application_Model_Show::getPrevCurrentNextOld($utcNow);
  99. $previousShowID = count($shows['previousShow'])>0?$shows['previousShow'][0]['instance_id']:null;
  100. $currentShowID = count($shows['currentShow'])>0?$shows['currentShow'][0]['instance_id']:null;
  101. $nextShowID = count($shows['nextShow'])>0?$shows['nextShow'][0]['instance_id']:null;
  102. $results = self::GetPrevCurrentNext($previousShowID, $currentShowID, $nextShowID, $utcNow);
  103. $range = array(
  104. "env" => APPLICATION_ENV,
  105. "schedulerTime" => $utcNow->format("Y-m-d H:i:s"),
  106. //Previous, current, next songs!
  107. "previous"=>$results['previous'] !=null?$results['previous']:(count($shows['previousShow'])>0?$shows['previousShow'][0]:null),
  108. "current"=>$results['current'] !=null?$results['current']:((count($shows['currentShow'])>0 && $shows['currentShow'][0]['record'] == 1)?$shows['currentShow'][0]:null),
  109. "next"=> $results['next'] !=null?$results['next']:(count($shows['nextShow'])>0?$shows['nextShow'][0]:null),
  110. //Current and next shows
  111. "currentShow"=>$shows['currentShow'],
  112. "nextShow"=>$shows['nextShow']
  113. );
  114. return $range;
  115. }
  116. /**
  117. * Queries the database for the set of schedules one hour before
  118. * and after the given time. If a show starts and ends within that
  119. * time that is considered the current show. Then the scheduled item
  120. * before it is the previous show, and the scheduled item after it
  121. * is the next show. This way the dashboard getCurrentPlaylist is
  122. * very fast. But if any one of the three show types are not found
  123. * through this mechanism a call is made to the old way of querying
  124. * the database to find the track info.
  125. **/
  126. public static function GetPrevCurrentNext($p_previousShowID, $p_currentShowID, $p_nextShowID, $utcNow)
  127. {
  128. $timeZone = new DateTimeZone("UTC"); //This function works entirely in UTC.
  129. assert(get_class($utcNow) === "DateTime");
  130. assert($utcNow->getTimeZone() == $timeZone);
  131. if ($p_previousShowID == null && $p_currentShowID == null && $p_nextShowID == null) {
  132. return;
  133. }
  134. $sql = "SELECT %%columns%% st.starts as starts, st.ends as ends,
  135. st.media_item_played as media_item_played, si.ends as show_ends
  136. %%tables%% WHERE ";
  137. $fileColumns = "ft.artist_name, ft.track_title, ";
  138. $fileJoin = "FROM cc_schedule st JOIN cc_files ft ON st.file_id = ft.id
  139. LEFT JOIN cc_show_instances si ON st.instance_id = si.id";
  140. $streamColumns = "ws.name AS artist_name, wm.liquidsoap_data AS track_title, ";
  141. $streamJoin = <<<SQL
  142. FROM cc_schedule AS st
  143. JOIN cc_webstream ws ON st.stream_id = ws.id
  144. LEFT JOIN cc_show_instances AS si ON st.instance_id = si.id
  145. LEFT JOIN cc_subjs AS sub ON sub.id = ws.creator_id
  146. LEFT JOIN
  147. (SELECT *
  148. FROM cc_webstream_metadata
  149. ORDER BY start_time DESC LIMIT 1) AS wm ON st.id = wm.instance_id
  150. SQL;
  151. $predicateArr = array();
  152. $paramMap = array();
  153. if (isset($p_previousShowID)) {
  154. $predicateArr[] = 'st.instance_id = :previousShowId';
  155. $paramMap[':previousShowId'] = $p_previousShowID;
  156. }
  157. if (isset($p_currentShowID)) {
  158. $predicateArr[] = 'st.instance_id = :currentShowId';
  159. $paramMap[':currentShowId'] = $p_currentShowID;
  160. }
  161. if (isset($p_nextShowID)) {
  162. $predicateArr[] = 'st.instance_id = :nextShowId';
  163. $paramMap[':nextShowId'] = $p_nextShowID;
  164. }
  165. $sql .= " (".implode(" OR ", $predicateArr).") ";
  166. $sql .= ' AND st.playout_status > 0 ORDER BY st.starts';
  167. $filesSql = str_replace("%%columns%%", $fileColumns, $sql);
  168. $filesSql = str_replace("%%tables%%", $fileJoin, $filesSql);
  169. $streamSql = str_replace("%%columns%%", $streamColumns, $sql);
  170. $streamSql = str_replace("%%tables%%", $streamJoin, $streamSql);
  171. $sql = "SELECT * FROM (($filesSql) UNION ($streamSql)) AS unioned ORDER BY starts";
  172. $rows = Application_Common_Database::prepareAndExecute($sql, $paramMap);
  173. $numberOfRows = count($rows);
  174. $results['previous'] = null;
  175. $results['current'] = null;
  176. $results['next'] = null;
  177. for ($i = 0; $i < $numberOfRows; ++$i) {
  178. // if the show is overbooked, then update the track end time to the end of the show time.
  179. if ($rows[$i]['ends'] > $rows[$i]["show_ends"]) {
  180. $rows[$i]['ends'] = $rows[$i]["show_ends"];
  181. }
  182. $curShowStartTime = new DateTime($rows[$i]['starts'], $timeZone);
  183. $curShowEndTime = new DateTime($rows[$i]['ends'], $timeZone);
  184. if (($curShowStartTime <= $utcNow) && ($curShowEndTime >= $utcNow)) {
  185. if ($i - 1 >= 0) {
  186. $results['previous'] = array("name"=>$rows[$i-1]["artist_name"]." - ".$rows[$i-1]["track_title"],
  187. "starts"=>$rows[$i-1]["starts"],
  188. "ends"=>$rows[$i-1]["ends"],
  189. "type"=>'track');
  190. }
  191. $results['current'] = array("name"=>$rows[$i]["artist_name"]." - ".$rows[$i]["track_title"],
  192. "starts"=>$rows[$i]["starts"],
  193. "ends"=> (($rows[$i]["ends"] > $rows[$i]["show_ends"]) ? $rows[$i]["show_ends"]: $rows[$i]["ends"]),
  194. "media_item_played"=>$rows[$i]["media_item_played"],
  195. "record"=>0,
  196. "type"=>'track');
  197. if (isset($rows[$i+1])) {
  198. $results['next'] = array("name"=>$rows[$i+1]["artist_name"]." - ".$rows[$i+1]["track_title"],
  199. "starts"=>$rows[$i+1]["starts"],
  200. "ends"=>$rows[$i+1]["ends"],
  201. "type"=>'track');
  202. }
  203. break;
  204. }
  205. if ($curShowEndTime < $utcNow ) {
  206. $previousIndex = $i;
  207. }
  208. if ($curShowStartTime > $utcNow) {
  209. $results['next'] = array("name"=>$rows[$i]["artist_name"]." - ".$rows[$i]["track_title"],
  210. "starts"=>$rows[$i]["starts"],
  211. "ends"=>$rows[$i]["ends"],
  212. "type"=>'track');
  213. break;
  214. }
  215. }
  216. //If we didn't find a a current show because the time didn't fit we may still have
  217. //found a previous show so use it.
  218. if ($results['previous'] === null && isset($previousIndex)) {
  219. $results['previous'] = array("name"=>$rows[$previousIndex]["artist_name"]." - ".$rows[$previousIndex]["track_title"],
  220. "starts"=>$rows[$previousIndex]["starts"],
  221. "ends"=>$rows[$previousIndex]["ends"]);;
  222. }
  223. return $results;
  224. }
  225. public static function GetLastScheduleItem($p_timeNow)
  226. {
  227. $sql = <<<SQL
  228. SELECT ft.artist_name,
  229. ft.track_title,
  230. st.starts AS starts,
  231. st.ends AS ends
  232. FROM cc_schedule st
  233. LEFT JOIN cc_files ft ON st.file_id = ft.id
  234. LEFT JOIN cc_show_instances sit ON st.instance_id = sit.id
  235. -- this and the next line are necessary since we can overbook shows.
  236. WHERE st.ends < TIMESTAMP :timeNow
  237. AND st.starts >= sit.starts
  238. AND st.starts < sit.ends
  239. ORDER BY st.ends DESC LIMIT 1;
  240. SQL;
  241. $row = Application_Common_Database::prepareAndExecute($sql, array(':timeNow'=>$p_timeNow));
  242. return $row;
  243. }
  244. public static function GetCurrentScheduleItem($p_timeNow, $p_instanceId)
  245. {
  246. /* Note that usually there will be one result returned. In some
  247. * rare cases two songs are returned. This happens when a track
  248. * that was overbooked from a previous show appears as if it
  249. * hasnt ended yet (track end time hasn't been reached yet). For
  250. * this reason, we need to get the track that starts later, as
  251. * this is the *real* track that is currently playing. So this
  252. * is why we are ordering by track start time. */
  253. $sql = "SELECT *"
  254. ." FROM cc_schedule st"
  255. ." LEFT JOIN cc_files ft"
  256. ." ON st.file_id = ft.id"
  257. ." WHERE st.starts <= TIMESTAMP :timeNow1"
  258. ." AND st.instance_id = :instanceId"
  259. ." AND st.ends > TIMESTAMP :timeNow2"
  260. ." ORDER BY st.starts DESC"
  261. ." LIMIT 1";
  262. $row = Application_Common_Database::prepareAndExecute($sql, array(':timeNow1'=>$p_timeNow, ':instanceId'=>$p_instanceId, ':timeNow2'=>$p_timeNow,));
  263. return $row;
  264. }
  265. public static function GetNextScheduleItem($p_timeNow)
  266. {
  267. $sql = "SELECT"
  268. ." ft.artist_name, ft.track_title,"
  269. ." st.starts as starts, st.ends as ends"
  270. ." FROM cc_schedule st"
  271. ." LEFT JOIN cc_files ft"
  272. ." ON st.file_id = ft.id"
  273. ." LEFT JOIN cc_show_instances sit"
  274. ." ON st.instance_id = sit.id"
  275. ." WHERE st.starts > TIMESTAMP :timeNow"
  276. ." AND st.starts >= sit.starts" //this and the next line are necessary since we can overbook shows.
  277. ." AND st.starts < sit.ends"
  278. ." ORDER BY st.starts"
  279. ." LIMIT 1";
  280. $row = Application_Common_Database::prepareAndExecute($sql, array(':timeNow'=>$p_timeNow));
  281. return $row;
  282. }
  283. /*
  284. *
  285. * @param DateTime $p_startDateTime
  286. *
  287. * @param DateTime $p_endDateTime
  288. *
  289. * @return array $scheduledItems
  290. *
  291. */
  292. public static function GetScheduleDetailItems($p_start, $p_end, $p_shows, $p_show_instances)
  293. {
  294. $p_start_str = $p_start->format("Y-m-d H:i:s");
  295. $p_end_str = $p_end->format("Y-m-d H:i:s");
  296. //We need to search 48 hours before and after the show times so that that we
  297. //capture all of the show's contents.
  298. $p_track_start= $p_start->sub(new DateInterval("PT48H"))->format("Y-m-d H:i:s");
  299. $p_track_end = $p_end->add(new DateInterval("PT48H"))->format("Y-m-d H:i:s");
  300. $templateSql = <<<SQL
  301. SELECT DISTINCT sched.starts AS sched_starts,
  302. sched.ends AS sched_ends,
  303. sched.id AS sched_id,
  304. sched.cue_in AS cue_in,
  305. sched.cue_out AS cue_out,
  306. sched.fade_in AS fade_in,
  307. sched.fade_out AS fade_out,
  308. sched.playout_status AS playout_status,
  309. sched.instance_id AS sched_instance_id,
  310. %%columns%%
  311. FROM (%%join%%)
  312. SQL;
  313. $filesColumns = <<<SQL
  314. ft.track_title AS file_track_title,
  315. ft.artist_name AS file_artist_name,
  316. ft.album_title AS file_album_title,
  317. ft.length AS file_length,
  318. ft.file_exists AS file_exists,
  319. ft.mime AS file_mime,
  320. ft.soundcloud_id AS soundcloud_id
  321. SQL;
  322. $filesJoin = <<<SQL
  323. cc_schedule AS sched
  324. JOIN cc_files AS ft ON (sched.file_id = ft.id
  325. AND ((sched.starts >= :fj_ts_1
  326. AND sched.starts < :fj_ts_2)
  327. OR (sched.ends > :fj_ts_3
  328. AND sched.ends <= :fj_ts_4)
  329. OR (sched.starts <= :fj_ts_5
  330. AND sched.ends >= :fj_ts_6))
  331. )
  332. SQL;
  333. $paramMap = array(
  334. ":fj_ts_1" => $p_track_start,
  335. ":fj_ts_2" => $p_track_end,
  336. ":fj_ts_3" => $p_track_start,
  337. ":fj_ts_4" => $p_track_end,
  338. ":fj_ts_5" => $p_track_start,
  339. ":fj_ts_6" => $p_track_end,
  340. );
  341. $filesSql = str_replace("%%columns%%",
  342. $filesColumns,
  343. $templateSql);
  344. $filesSql= str_replace("%%join%%",
  345. $filesJoin,
  346. $filesSql);
  347. $streamColumns = <<<SQL
  348. ws.name AS file_track_title,
  349. sub.login AS file_artist_name,
  350. ws.description AS file_album_title,
  351. ws.length AS file_length,
  352. 't'::BOOL AS file_exists,
  353. ws.mime AS file_mime,
  354. (SELECT NULL::integer AS soundcloud_id)
  355. SQL;
  356. $streamJoin = <<<SQL
  357. cc_schedule AS sched
  358. JOIN cc_webstream AS ws ON (sched.stream_id = ws.id
  359. AND ((sched.starts >= :sj_ts_1
  360. AND sched.starts < :sj_ts_2)
  361. OR (sched.ends > :sj_ts_3
  362. AND sched.ends <= :sj_ts_4)
  363. OR (sched.starts <= :sj_ts_5
  364. AND sched.ends >= :sj_ts_6))
  365. )
  366. LEFT JOIN cc_subjs AS sub ON (ws.creator_id = sub.id)
  367. SQL;
  368. $map = array(
  369. ":sj_ts_1" => $p_track_start,
  370. ":sj_ts_2" => $p_track_end,
  371. ":sj_ts_3" => $p_track_start,
  372. ":sj_ts_4" => $p_track_end,
  373. ":sj_ts_5" => $p_track_start,
  374. ":sj_ts_6" => $p_track_end,
  375. );
  376. $paramMap = $paramMap + $map;
  377. $streamSql = str_replace("%%columns%%",
  378. $streamColumns,
  379. $templateSql);
  380. $streamSql = str_replace("%%join%%",
  381. $streamJoin,
  382. $streamSql);
  383. $showPredicate = "";
  384. if (count($p_shows) > 0) {
  385. $params = array();
  386. $map = array();
  387. for ($i = 0, $len = count($p_shows); $i < $len; $i++) {
  388. $holder = ":show_".$i;
  389. $params[] = $holder;
  390. $map[$holder] = $p_shows[$i];
  391. }
  392. $showPredicate = " AND show_id IN (".implode(",", $params).")";
  393. $paramMap = $paramMap + $map;
  394. } else if (count($p_show_instances) > 0) {
  395. $showPredicate = " AND si.id IN (".implode(",", $p_show_instances).")";
  396. }
  397. $sql = <<<SQL
  398. SELECT showt.name AS show_name,
  399. showt.color AS show_color,
  400. showt.background_color AS show_background_color,
  401. showt.id AS show_id,
  402. showt.linked AS linked,
  403. si.starts AS si_starts,
  404. si.ends AS si_ends,
  405. si.time_filled AS si_time_filled,
  406. si.record AS si_record,
  407. si.rebroadcast AS si_rebroadcast,
  408. si.instance_id AS parent_show,
  409. si.id AS si_id,
  410. si.last_scheduled AS si_last_scheduled,
  411. si.file_id AS si_file_id,
  412. *
  413. FROM (($filesSql) UNION ($streamSql)) as temp
  414. RIGHT JOIN cc_show_instances AS si ON (si.id = sched_instance_id)
  415. JOIN cc_show AS showt ON (showt.id = si.show_id)
  416. WHERE si.modified_instance = FALSE
  417. $showPredicate
  418. AND ((si.starts >= :ts_1
  419. AND si.starts < :ts_2)
  420. OR (si.ends > :ts_3
  421. AND si.ends <= :ts_4)
  422. OR (si.starts <= :ts_5
  423. AND si.ends >= :ts_6))
  424. ORDER BY si_starts,
  425. sched_starts;
  426. SQL;
  427. $map = array(
  428. ":ts_1" => $p_start_str,
  429. ":ts_2" => $p_end_str,
  430. ":ts_3" => $p_start_str,
  431. ":ts_4" => $p_end_str,
  432. ":ts_5" => $p_start_str,
  433. ":ts_6" => $p_end_str,
  434. );
  435. $paramMap = $paramMap + $map;
  436. $rows = Application_Common_Database::prepareAndExecute(
  437. $sql,
  438. $paramMap,
  439. Application_Common_Database::ALL
  440. );
  441. return $rows;
  442. }
  443. public static function UpdateMediaPlayedStatus($p_id)
  444. {
  445. $sql = "UPDATE cc_schedule"
  446. ." SET media_item_played=TRUE";
  447. // we need to update 'broadcasted' column as well
  448. // check the current switch status
  449. $live_dj = Application_Model_Preference::GetSourceSwitchStatus('live_dj') == 'on';
  450. $master_dj = Application_Model_Preference::GetSourceSwitchStatus('master_dj') == 'on';
  451. $scheduled_play = Application_Model_Preference::GetSourceSwitchStatus('scheduled_play') == 'on';
  452. if (!$live_dj && !$master_dj && $scheduled_play) {
  453. $sql .= ", broadcasted=1";
  454. }
  455. $sql .= " WHERE id=:pid";
  456. $map = array(":pid" => $p_id);
  457. Application_Common_Database::prepareAndExecute($sql, $map,
  458. Application_Common_Database::EXECUTE);
  459. }
  460. public static function UpdateBrodcastedStatus($dateTime, $value)
  461. {
  462. $now = $dateTime->format("Y-m-d H:i:s");
  463. $sql = <<<SQL
  464. UPDATE cc_schedule
  465. SET broadcasted=:broadcastedValue
  466. WHERE starts <= :starts::TIMESTAMP
  467. AND ends >= :ends::TIMESTAMP
  468. SQL;
  469. $retVal = Application_Common_Database::prepareAndExecute($sql, array(
  470. ':broadcastedValue' => $value,
  471. ':starts' => $now,
  472. ':ends' => $now), 'execute');
  473. return $retVal;
  474. }
  475. public static function getSchduledPlaylistCount()
  476. {
  477. $sql = "SELECT count(*) as cnt FROM cc_schedule";
  478. $res = Application_Common_Database::prepareAndExecute($sql, array(),
  479. Application_Common_Database::COLUMN);
  480. return $res;
  481. }
  482. /**
  483. * Convert a time string in the format "YYYY-MM-DD HH:mm:SS"
  484. * to "YYYY-MM-DD-HH-mm-SS".
  485. *
  486. * @param string $p_time
  487. * @return string
  488. */
  489. private static function AirtimeTimeToPypoTime($p_time)
  490. {
  491. $p_time = substr($p_time, 0, 19);
  492. $p_time = str_replace(" ", "-", $p_time);
  493. $p_time = str_replace(":", "-", $p_time);
  494. return $p_time;
  495. }
  496. /**
  497. * Convert a time string in the format "YYYY-MM-DD-HH-mm-SS" to
  498. * "YYYY-MM-DD HH:mm:SS".
  499. *
  500. * @param string $p_time
  501. * @return string
  502. */
  503. private static function PypoTimeToAirtimeTime($p_time)
  504. {
  505. $t = explode("-", $p_time);
  506. return $t[0]."-".$t[1]."-".$t[2]." ".$t[3].":".$t[4].":00";
  507. }
  508. /**
  509. * Return true if the input string is in the format YYYY-MM-DD-HH-mm
  510. *
  511. * @param string $p_time
  512. * @return boolean
  513. */
  514. public static function ValidPypoTimeFormat($p_time)
  515. {
  516. $t = explode("-", $p_time);
  517. if (count($t) != 5) {
  518. return false;
  519. }
  520. foreach ($t as $part) {
  521. if (!is_numeric($part)) {
  522. return false;
  523. }
  524. }
  525. return true;
  526. }
  527. /**
  528. * Converts a time value as a string (with format HH:MM:SS.mmmmmm) to
  529. * millisecs.
  530. *
  531. * @param string $p_time
  532. * @return int
  533. */
  534. public static function WallTimeToMillisecs($p_time)
  535. {
  536. $t = explode(":", $p_time);
  537. $millisecs = 0;
  538. if (strpos($t[2], ".")) {
  539. $secParts = explode(".", $t[2]);
  540. $millisecs = $secParts[1];
  541. $millisecs = str_pad(substr($millisecs, 0, 3),3, '0');
  542. $millisecs = intval($millisecs);
  543. $seconds = intval($secParts[0]);
  544. } else {
  545. $seconds = intval($t[2]);
  546. }
  547. $ret = $millisecs + ($seconds * 1000) + ($t[1] * 60 * 1000) + ($t[0] * 60 * 60 * 1000);
  548. return $ret;
  549. }
  550. /**
  551. * Returns an array of schedule items from cc_schedule table. Tries
  552. * to return at least 3 items (if they are available). The parameters
  553. * $p_startTime and $p_endTime specify the range. Schedule items returned
  554. * do not have to be entirely within this range. It is enough that the end
  555. * or beginning of the scheduled item is in the range.
  556. *
  557. *
  558. * @param string $p_startTime
  559. * In the format YYYY-MM-DD HH:MM:SS.nnnnnn
  560. * @param string $p_endTime
  561. * In the format YYYY-MM-DD HH:MM:SS.nnnnnn
  562. * @return array
  563. * Returns null if nothing found, else an array of associative
  564. * arrays representing each row.
  565. */
  566. public static function getItems($p_startTime, $p_endTime)
  567. {
  568. $baseQuery = <<<SQL
  569. SELECT st.file_id AS file_id,
  570. st.id AS id,
  571. st.instance_id AS instance_id,
  572. st.starts AS start,
  573. st.ends AS end,
  574. st.cue_in AS cue_in,
  575. st.cue_out AS cue_out,
  576. st.fade_in AS fade_in,
  577. st.fade_out AS fade_out,
  578. si.starts AS show_start,
  579. si.ends AS show_end,
  580. s.name AS show_name,
  581. f.id AS file_id,
  582. f.replay_gain AS replay_gain,
  583. ws.id AS stream_id,
  584. ws.url AS url
  585. FROM cc_schedule AS st
  586. LEFT JOIN cc_show_instances AS si ON st.instance_id = si.id
  587. LEFT JOIN cc_show AS s ON s.id = si.show_id
  588. LEFT JOIN cc_files AS f ON st.file_id = f.id
  589. LEFT JOIN cc_webstream AS ws ON st.stream_id = ws.id
  590. SQL;
  591. $predicates = <<<SQL
  592. WHERE st.ends > :startTime1
  593. AND st.starts < :endTime
  594. AND st.playout_status > 0
  595. AND si.ends > :startTime2
  596. AND si.modified_instance = 'f'
  597. ORDER BY st.starts
  598. SQL;
  599. $sql = $baseQuery." ".$predicates;
  600. $rows = Application_Common_Database::prepareAndExecute($sql, array(
  601. ':startTime1' => $p_startTime,
  602. ':endTime' => $p_endTime,
  603. ':startTime2' => $p_startTime));
  604. if (count($rows) < 3) {
  605. $dt = new DateTime("@".time());
  606. $dt->add(new DateInterval("PT24H"));
  607. $range_end = $dt->format("Y-m-d H:i:s");
  608. $predicates = <<<SQL
  609. WHERE st.ends > :startTime1
  610. AND st.starts < :rangeEnd
  611. AND st.playout_status > 0
  612. AND si.ends > :startTime2
  613. AND si.modified_instance = 'f'
  614. ORDER BY st.starts LIMIT 3
  615. SQL;
  616. $sql = $baseQuery." ".$predicates." ";
  617. $rows = Application_Common_Database::prepareAndExecute($sql,
  618. array(
  619. ':startTime1' => $p_startTime,
  620. ':rangeEnd' => $range_end,
  621. ':startTime2' => $p_startTime));
  622. }
  623. return $rows;
  624. }
  625. /**
  626. * This function will ensure that an existing index in the
  627. * associative array is never overwritten, instead appending
  628. * _0, _1, _2, ... to the end of the key to make sure it is unique
  629. */
  630. private static function appendScheduleItem(&$data, $time, $item)
  631. {
  632. $key = $time;
  633. $i = 0;
  634. while (array_key_exists($key, $data["media"])) {
  635. $key = "{$time}_{$i}";
  636. $i++;
  637. }
  638. $data["media"][$key] = $item;
  639. }
  640. private static function createInputHarborKickTimes(&$data, $range_start, $range_end)
  641. {
  642. $utcTimeZone = new DateTimeZone("UTC");
  643. $kick_times = Application_Model_ShowInstance::GetEndTimeOfNextShowWithLiveDJ($range_start, $range_end);
  644. foreach ($kick_times as $kick_time_info) {
  645. $kick_time = $kick_time_info['ends'];
  646. $temp = explode('.', Application_Model_Preference::GetDefaultTransitionFade());
  647. // we round down transition time since PHP cannot handle millisecond. We need to
  648. // handle this better in the future
  649. $transition_time = intval($temp[0]);
  650. $switchOffDataTime = new DateTime($kick_time, $utcTimeZone);
  651. $switch_off_time = $switchOffDataTime->sub(new DateInterval('PT'.$transition_time.'S'));
  652. $switch_off_time = $switch_off_time->format("Y-m-d H:i:s");
  653. $kick_start = self::AirtimeTimeToPypoTime($kick_time);
  654. $data["media"][$kick_start]['start'] = $kick_start;
  655. $data["media"][$kick_start]['end'] = $kick_start;
  656. $data["media"][$kick_start]['event_type'] = "kick_out";
  657. $data["media"][$kick_start]['type'] = "event";
  658. $data["media"][$kick_start]['independent_event'] = true;
  659. if ($kick_time !== $switch_off_time) {
  660. $switch_start = self::AirtimeTimeToPypoTime($switch_off_time);
  661. $data["media"][$switch_start]['start'] = $switch_start;
  662. $data["media"][$switch_start]['end'] = $switch_start;
  663. $data["media"][$switch_start]['event_type'] = "switch_off";
  664. $data["media"][$switch_start]['type'] = "event";
  665. $data["media"][$switch_start]['independent_event'] = true;
  666. }
  667. }
  668. }
  669. private static function createFileScheduleEvent(&$data, $item, $media_id, $uri)
  670. {
  671. $start = self::AirtimeTimeToPypoTime($item["start"]);
  672. $end = self::AirtimeTimeToPypoTime($item["end"]);
  673. list(,,,$start_hour,,) = explode("-", $start);
  674. list(,,,$end_hour,,) = explode("-", $end);
  675. $same_hour = $start_hour == $end_hour;
  676. $independent_event = !$same_hour;
  677. $replay_gain = is_null($item["replay_gain"]) ? "0": $item["replay_gain"];
  678. $replay_gain += Application_Model_Preference::getReplayGainModifier();
  679. if ( !Application_Model_Preference::GetEnableReplayGain() ) {
  680. $replay_gain = 0;
  681. }
  682. $schedule_item = array(
  683. 'id' => $media_id,
  684. 'type' => 'file',
  685. 'row_id' => $item["id"],
  686. 'uri' => $uri,
  687. 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]),
  688. 'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]),
  689. 'cue_in' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_in"]),
  690. 'cue_out' => Application_Common_DateHelper::CalculateLengthInSeconds($item["cue_out"]),
  691. 'start' => $start,
  692. 'end' => $end,
  693. 'show_name' => $item["show_name"],
  694. 'replay_gain' => $replay_gain,
  695. 'independent_event' => $independent_event,
  696. );
  697. if ($schedule_item['cue_in'] > $schedule_item['cue_out']) {
  698. $schedule_item['cue_in'] = $schedule_item['cue_out'];
  699. }
  700. self::appendScheduleItem($data, $start, $schedule_item);
  701. }
  702. private static function createStreamScheduleEvent(&$data, $item, $media_id, $uri)
  703. {
  704. $start = self::AirtimeTimeToPypoTime($item["start"]);
  705. $end = self::AirtimeTimeToPypoTime($item["end"]);
  706. //create an event to start stream buffering 5 seconds ahead of the streams actual time.
  707. $buffer_start = new DateTime($item["start"], new DateTimeZone('UTC'));
  708. $buffer_start->sub(new DateInterval("PT5S"));
  709. $stream_buffer_start = self::AirtimeTimeToPypoTime($buffer_start->format("Y-m-d H:i:s"));
  710. $schedule_item = array(
  711. 'start' => $stream_buffer_start,
  712. 'end' => $stream_buffer_start,
  713. 'uri' => $uri,
  714. 'row_id' => $item["id"],
  715. 'type' => 'stream_buffer_start',
  716. 'independent_event' => true
  717. );
  718. self::appendScheduleItem($data, $start, $schedule_item);
  719. $schedule_item = array(
  720. 'id' => $media_id,
  721. 'type' => 'stream_output_start',
  722. 'row_id' => $item["id"],
  723. 'uri' => $uri,
  724. 'start' => $start,
  725. 'end' => $end,
  726. 'show_name' => $item["show_name"],
  727. 'independent_event' => true
  728. );
  729. self::appendScheduleItem($data, $start, $schedule_item);
  730. //since a stream never ends we have to insert an additional "kick stream" event. The "start"
  731. //time of this event is the "end" time of the stream minus 1 second.
  732. $dt = new DateTime($item["end"], new DateTimeZone('UTC'));
  733. $dt->sub(new DateInterval("PT1S"));
  734. $stream_end = self::AirtimeTimeToPypoTime($dt->format("Y-m-d H:i:s"));
  735. $schedule_item = array(
  736. 'start' => $stream_end,
  737. 'end' => $stream_end,
  738. 'uri' => $uri,
  739. 'type' => 'stream_buffer_end',
  740. 'row_id' => $item["id"],
  741. 'independent_event' => true
  742. );
  743. self::appendScheduleItem($data, $stream_end, $schedule_item);
  744. $schedule_item = array(
  745. 'start' => $stream_end,
  746. 'end' => $stream_end,
  747. 'uri' => $uri,
  748. 'type' => 'stream_output_end',
  749. 'independent_event' => true
  750. );
  751. self::appendScheduleItem($data, $stream_end, $schedule_item);
  752. }
  753. private static function getRangeStartAndEnd($p_fromDateTime, $p_toDateTime)
  754. {
  755. $CC_CONFIG = Config::getConfig();
  756. $utcTimeZone = new DateTimeZone('UTC');
  757. /* if $p_fromDateTime and $p_toDateTime function parameters are null,
  758. then set range * from "now" to "now + 24 hours". */
  759. if (is_null($p_fromDateTime)) {
  760. $t1 = new DateTime("@".time(), $utcTimeZone);
  761. $range_start = $t1->format("Y-m-d H:i:s");
  762. } else {
  763. $range_start = Application_Model_Schedule::PypoTimeToAirtimeTime($p_fromDateTime);
  764. }
  765. if (is_null($p_fromDateTime)) {
  766. $t2 = new DateTime("@".time(), $utcTimeZone);
  767. $cache_ahead_hours = $CC_CONFIG["cache_ahead_hours"];
  768. if (is_numeric($cache_ahead_hours)) {
  769. //make sure we are not dealing with a float
  770. $cache_ahead_hours = intval($cache_ahead_hours);
  771. } else {
  772. $cache_ahead_hours = 1;
  773. }
  774. $t2->add(new DateInterval("PT".$cache_ahead_hours."H"));
  775. $range_end = $t2->format("Y-m-d H:i:s");
  776. } else {
  777. $range_end = Application_Model_Schedule::PypoTimeToAirtimeTime($p_toDateTime);
  778. }
  779. return array($range_start, $range_end);
  780. }
  781. private static function createScheduledEvents(&$data, $range_start, $range_end)
  782. {
  783. $utcTimeZone = new DateTimeZone("UTC");
  784. $items = self::getItems($range_start, $range_end);
  785. foreach ($items as $item) {
  786. $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone);
  787. $trackStartDateTime = new DateTime($item["start"], $utcTimeZone);
  788. $trackEndDateTime = new DateTime($item["end"], $utcTimeZone);
  789. if ($trackStartDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) {
  790. //do not send any tracks that start past their show's end time
  791. continue;
  792. }
  793. if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()) {
  794. $di = $trackStartDateTime->diff($showEndDateTime);
  795. $item["cue_out"] = $di->format("%H:%i:%s").".000";
  796. $item["end"] = $showEndDateTime->format("Y-m-d H:i:s");
  797. }
  798. if (!is_null($item['file_id'])) {
  799. //row is from "file"
  800. $media_id = $item['file_id'];
  801. $storedFile = Application_Model_StoredFile::RecallById($media_id);
  802. $uri = $storedFile->getFilePath();
  803. self::createFileScheduleEvent($data, $item, $media_id, $uri);
  804. }
  805. elseif (!is_null($item['stream_id'])) {
  806. //row is type "webstream"
  807. $media_id = $item['stream_id'];
  808. $uri = $item['url'];
  809. self::createStreamScheduleEvent($data, $item, $media_id, $uri);
  810. }
  811. else {
  812. throw new Exception("Unknown schedule type: ".print_r($item, true));
  813. }
  814. }
  815. }
  816. /* Check if two events are less than or equal to 1 second apart
  817. */
  818. public static function areEventsLinked($event1, $event2) {
  819. $dt1 = DateTime::createFromFormat("Y-m-d-H-i-s", $event1['start']);
  820. $dt2 = DateTime::createFromFormat("Y-m-d-H-i-s", $event2['start']);
  821. $seconds = $dt2->getTimestamp() - $dt1->getTimestamp();
  822. return $seconds <= 1;
  823. }
  824. /**
  825. * Streams are a 4 stage process.
  826. * 1) start buffering stream 5 seconds ahead of its start time
  827. * 2) at the start time tell liquidsoap to switch to this source
  828. * 3) at the end time, tell liquidsoap to stop reading this stream
  829. * 4) at the end time, tell liquidsoap to switch away from input.http source.
  830. *
  831. * When we have two streams back-to-back, some of these steps are unnecessary
  832. * for the second stream. Instead of sending commands 1,2,3,4,1,2,3,4 we should
  833. * send 1,2,1,2,3,4 - We don't need to tell liquidsoap to stop reading (#3), because #1
  834. * of the next stream implies this when we pass in a new url. We also don't need #4.
  835. *
  836. * There's a special case here is well. When the back-to-back streams are the same, we
  837. * can collapse the instructions 1,2,(3,4,1,2),3,4 to 1,2,3,4. We basically cut out the
  838. * middle part. This function handles this.
  839. */
  840. private static function foldData(&$data)
  841. {
  842. $previous_key = null;
  843. $previous_val = null;
  844. $previous_previous_key = null;
  845. $previous_previous_val = null;
  846. $previous_previous_previous_key = null;
  847. $previous_previous_previous_val = null;
  848. foreach ($data as $k => $v) {
  849. if ($v["type"] == "stream_output_start"
  850. && !is_null($previous_previous_val)
  851. && $previous_previous_val["type"] == "stream_output_end"
  852. && self::areEventsLinked($previous_previous_val, $v)) {
  853. unset($data[$previous_previous_previous_key]);
  854. unset($data[$previous_previous_key]);
  855. unset($data[$previous_key]);
  856. if ($previous_previous_val['uri'] == $v['uri']) {
  857. unset($data[$k]);
  858. }
  859. }
  860. $previous_previous_previous_key = $previous_previous_key;
  861. $previous_previous_previous_val = $previous_previous_val;
  862. $previous_previous_key = $previous_key;
  863. $previous_previous_val = $previous_val;
  864. $previous_key = $k;
  865. $previous_val = $v;
  866. }
  867. }
  868. public static function getSchedule($p_fromDateTime = null, $p_toDateTime = null)
  869. {
  870. //generate repeating shows if we are fetching the schedule
  871. //for days beyond the shows_populated_until value in cc_pref
  872. $needScheduleUntil = $p_toDateTime;
  873. if (is_null($needScheduleUntil)) {
  874. $needScheduleUntil = new DateTime("now", new DateTimeZone("UTC"));
  875. $needScheduleUntil->add(new DateInterval("P1D"));
  876. }
  877. Application_Model_Show::createAndFillShowInstancesPastPopulatedUntilDate($needScheduleUntil);
  878. list($range_start, $range_end) = self::getRangeStartAndEnd($p_fromDateTime, $p_toDateTime);
  879. $data = array();
  880. $data["media"] = array();
  881. //Harbor kick times *MUST* be ahead of schedule events, so that pypo
  882. //executes them first.
  883. self::createInputHarborKickTimes($data, $range_start, $range_end);
  884. self::createScheduledEvents($data, $range_start, $range_end);
  885. //self::foldData($data["media"]);
  886. return $data;
  887. }
  888. public static function deleteAll()
  889. {
  890. $sql = "TRUNCATE TABLE cc_schedule";
  891. Application_Common_Database::prepareAndExecute($sql, array(),
  892. Application_Common_Database::EXECUTE);
  893. }
  894. public static function deleteWithFileId($fileId)
  895. {
  896. $sql = "DELETE FROM cc_schedule WHERE file_id=:file_id";
  897. Application_Common_Database::prepareAndExecute($sql, array(':file_id'=>$fileId), 'execute');
  898. }
  899. public static function checkOverlappingShows($show_start, $show_end,
  900. $update=false, $instanceId=null, $showId=null)
  901. {
  902. //if the show instance does not exist or was deleted, return false
  903. if (!is_null($showId)) {
  904. $ccShowInstance = CcShowInstancesQuery::create()
  905. ->filterByDbShowId($showId)
  906. ->filterByDbStarts($show_start->format("Y-m-d H:i:s"))
  907. ->findOne();
  908. } elseif (!is_null($instanceId)) {
  909. $ccShowInstance = CcShowInstancesQuery::create()
  910. ->filterByDbId($instanceId)
  911. ->findOne();
  912. }
  913. if ($update && ($ccShowInstance && $ccShowInstance->getDbModifiedInstance() == true)) {
  914. return false;
  915. }
  916. $overlapping = false;
  917. $params = array(
  918. ':show_end1' => $show_end->format('Y-m-d H:i:s'),
  919. ':show_end2' => $show_end->format('Y-m-d H:i:s'),
  920. ':show_end3' => $show_end->format('Y-m-d H:i:s')
  921. );
  922. /* If a show is being edited, exclude it from the query
  923. * In both cases (new and edit) we only grab shows that
  924. * are scheduled 2 days prior
  925. */
  926. if ($update) {
  927. $sql = <<<SQL
  928. SELECT id,
  929. starts,
  930. ends
  931. FROM cc_show_instances
  932. WHERE (ends <= :show_end1
  933. OR starts <= :show_end2)
  934. AND date(starts) >= (date(:show_end3) - INTERVAL '2 days')
  935. AND modified_instance = FALSE
  936. SQL;
  937. if (is_null($showId)) {
  938. $sql .= <<<SQL
  939. AND id != :instanceId
  940. ORDER BY ends
  941. SQL;
  942. $params[':instanceId'] = $instanceId;
  943. } else {
  944. $sql .= <<<SQL
  945. AND show_id != :showId
  946. ORDER BY ends
  947. SQL;
  948. $params[':showId'] = $showId;
  949. }
  950. $rows = Application_Common_Database::prepareAndExecute($sql, $params, 'all');
  951. } else {
  952. $sql = <<<SQL
  953. SELECT id,
  954. starts,
  955. ends
  956. FROM cc_show_instances
  957. WHERE (ends <= :show_end1
  958. OR starts <= :show_end2)
  959. AND date(starts) >= (date(:show_end3) - INTERVAL '2 days')
  960. AND modified_instance = FALSE
  961. ORDER BY ends
  962. SQL;
  963. $rows = Application_Common_Database::prepareAndExecute($sql, array(
  964. ':show_end1' => $show_end->format('Y-m-d H:i:s'),
  965. ':show_end2' => $show_end->format('Y-m-d H:i:s'),
  966. ':show_end3' => $show_end->format('Y-m-d H:i:s')), 'all');
  967. }
  968. foreach ($rows as $row) {
  969. $start = new DateTime($row["starts"], new DateTimeZone('UTC'));
  970. $end = new DateTime($row["ends"], new DateTimeZone('UTC'));
  971. if ($show_start->getTimestamp() < $end->getTimestamp() &&
  972. $show_end->getTimestamp() > $start->getTimestamp()) {
  973. $overlapping = true;
  974. break;
  975. }
  976. }
  977. return $overlapping;
  978. }
  979. public static function GetType($p_scheduleId){
  980. $scheduledItem = CcScheduleQuery::create()->findPK($p_scheduleId);
  981. if ($scheduledItem->getDbFileId() == null) {
  982. return 'webstream';
  983. } else {
  984. return 'file';
  985. }
  986. }
  987. public static function GetFileId($p_scheduleId)
  988. {
  989. $scheduledItem = CcScheduleQuery::create()->findPK($p_scheduleId);
  990. return $scheduledItem->getDbFileId();
  991. }
  992. public static function GetStreamId($p_scheduleId)
  993. {
  994. $scheduledItem = CcScheduleQuery::create()->findPK($p_scheduleId);
  995. return $scheduledItem->getDbStreamId();
  996. }
  997. }