From c5c1dce4d6354f13563204ba7e656ee5cb491de2 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 22 Feb 2012 17:29:49 -0500 Subject: [PATCH 01/39] CC-3336: Refactor schedule API used by pypo -initial commit --- .../application/controllers/ApiController.php | 2 + airtime_mvc/application/models/Schedule.php | 184 ++++++++++-------- 2 files changed, 104 insertions(+), 82 deletions(-) diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index f9628a78a..6e8aa2b90 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -276,12 +276,14 @@ class ApiController extends Zend_Controller_Action $api_key = $this->_getParam('api_key'); + /* if(!in_array($api_key, $CC_CONFIG["apiKey"])) { header('HTTP/1.0 401 Unauthorized'); print 'You are not allowed to access this resource. '; exit; } + * */ PEAR::setErrorHandling(PEAR_ERROR_RETURN); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 1c2100d35..2256cf629 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -46,87 +46,6 @@ class Application_Model_Schedule { } - /** - * Returns array indexed by: - * "playlistId"/"playlist_id" (aliases to the same thing) - * "start"/"starts" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn - * "end"/"ends" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn - * "group_id"/"id" (aliases to the same thing) - * "clip_length" (for audio clips this is the length of the audio clip, - * for playlists this is the length of the entire playlist) - * "name" (playlist only) - * "creator" (playlist only) - * "file_id" (audioclip only) - * "count" (number of items in the playlist, always 1 for audioclips. - * Note that playlists with one item will also have count = 1. - * - * @param string $p_fromDateTime - * In the format YYYY-MM-DD HH:MM:SS.nnnnnn - * @param string $p_toDateTime - * In the format YYYY-MM-DD HH:MM:SS.nnnnnn - * @param boolean $p_playlistsOnly - * Retrieve playlists as a single item. - * @return array - * Returns empty array if nothing found - */ - - public static function GetItems($p_currentDateTime, $p_toDateTime, $p_playlistsOnly = true) - { - global $CC_CONFIG, $CC_DBC; - $rows = array(); - if (!$p_playlistsOnly) { - $sql = "SELECT * FROM ".$CC_CONFIG["scheduleTable"] - ." WHERE (starts >= TIMESTAMP '$p_currentDateTime') " - ." AND (ends <= TIMESTAMP '$p_toDateTime')"; - $rows = $CC_DBC->GetAll($sql); - foreach ($rows as &$row) { - $row["count"] = "1"; - $row["playlistId"] = $row["playlist_id"]; - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - $row["id"] = $row["group_id"]; - } - } else { - $sql = "SELECT MIN(pt.creator) AS creator," - ." st.group_id," - ." SUM(st.clip_length) AS clip_length," - ." MIN(st.file_id) AS file_id," - ." COUNT(*) as count," - ." MIN(st.playlist_id) AS playlist_id," - ." MIN(st.starts) AS starts," - ." MAX(st.ends) AS ends," - ." MIN(sh.name) AS show_name," - ." MIN(si.starts) AS show_start," - ." MAX(si.ends) AS show_end" - ." FROM $CC_CONFIG[scheduleTable] as st" - ." LEFT JOIN $CC_CONFIG[playListTable] as pt" - ." ON st.playlist_id = pt.id" - ." LEFT JOIN $CC_CONFIG[showInstances] as si" - ." ON st.instance_id = si.id" - ." LEFT JOIN $CC_CONFIG[showTable] as sh" - ." ON si.show_id = sh.id" - //The next line ensures we only get songs that haven't ended yet - ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" - ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" - //next line makes sure that we aren't returning items that - //are past the show's scheduled timeslot. - ." AND (st.starts < si.ends)" - ." GROUP BY st.group_id" - ." ORDER BY starts"; - - $rows = $CC_DBC->GetAll($sql); - if (!PEAR::isError($rows)) { - foreach ($rows as &$row) { - $row["playlistId"] = $row["playlist_id"]; - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - $row["id"] = $row["group_id"]; - } - } - } - return $rows; - } - /** * Returns data related to the scheduled items. * @@ -473,7 +392,108 @@ class Application_Model_Schedule { } return $diff; } + + /** + * Returns array indexed by: + * "playlistId"/"playlist_id" (aliases to the same thing) + * "start"/"starts" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn + * "end"/"ends" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn + * "group_id"/"id" (aliases to the same thing) + * "clip_length" (for audio clips this is the length of the audio clip, + * for playlists this is the length of the entire playlist) + * "name" (playlist only) + * "creator" (playlist only) + * "file_id" (audioclip only) + * "count" (number of items in the playlist, always 1 for audioclips. + * Note that playlists with one item will also have count = 1. + * + * @param string $p_fromDateTime + * In the format YYYY-MM-DD HH:MM:SS.nnnnnn + * @param string $p_toDateTime + * In the format YYYY-MM-DD HH:MM:SS.nnnnnn + * @param boolean $p_playlistsOnly + * Retrieve playlists as a single item. + * @return array + * Returns null if nothing found + */ + public static function GetItems($p_currentDateTime, $p_toDateTime) + { + global $CC_CONFIG, $CC_DBC; + $rows = array(); + + $sql = "SELECT st.file_id AS file_id," + ." st.starts AS starts," + ." st.ends AS ends," + ." si.starts as show_start," + ." si.ends as show_end" + ." FROM $CC_CONFIG[scheduleTable] as st" + ." LEFT JOIN $CC_CONFIG[showInstances] as si" + ." ON st.instance_id = si.id" + ." ORDER BY starts"; + + + /* + $sql = "SELECT pt.creator as creator" + ." st.file_id AS file_id" + ." st.starts AS starts" + ." st.ends AS ends" + ." st.name as show_name" + ." si.starts as show_start" + ." si.ends as show_end" + ." FROM $CC_CONFIG[scheduleTable] as st" + ." LEFT JOIN $CC_CONFIG[showInstances] as si" + ." ON st.instance_id = si.id" + ." LEFT JOIN $CC_CONFIG[showTable] as sh" + ." ON si.show_id = sh.id" + //The next line ensures we only get songs that haven't ended yet + ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" + ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" + //next line makes sure that we aren't returning items that + //are past the show's scheduled timeslot. + ." AND (st.starts < si.ends)" + ." ORDER BY starts"; + * */ + + $rows = $CC_DBC->GetAll($sql); + if (!PEAR::isError($rows)) { + foreach ($rows as &$row) { + $row["start"] = $row["starts"]; + $row["end"] = $row["ends"]; + } + } + + return $rows; + } + + + public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null){ + + global $CC_CONFIG, $CC_DBC; + + /* 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()); + $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()); + $t2->add(new DateInterval("PT24H")); + $range_end = $t2->format("Y-m-d H:i:s"); + } else { + $range_end = Application_Model_Schedule::PypoTimeToAirtimeTime($p_toDateTime); + } + + // Scheduler wants everything in a playlist + $data = Application_Model_Schedule::GetItems($range_start, $range_end); + + Logging::log(print_r($data, true)); + + return $data; + } /** * Export the schedule in json formatted for pypo (the liquidsoap scheduler) @@ -483,7 +503,7 @@ class Application_Model_Schedule { * @param string $p_toDateTime * In the format "YYYY-MM-DD-HH-mm-SS" */ - public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null) + public static function GetScheduledPlaylistsOld($p_fromDateTime = null, $p_toDateTime = null) { global $CC_CONFIG, $CC_DBC; From da012af6edb63be1dc403e8b6919e621d90a8d4c Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 22 Feb 2012 20:41:24 -0500 Subject: [PATCH 02/39] CC-3336: Refactor schedule API used by pypo -removed export_source -rewrote GetScheduledPlaylists() --- airtime_mvc/application/models/Schedule.php | 47 +++++++++++++++++++-- python_apps/pypo/pypo-cli.py | 9 +--- python_apps/pypo/pypofetch.py | 26 +++++------- python_apps/pypo/pypopush.py | 8 +--- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 2256cf629..40453c1b5 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -466,6 +466,19 @@ class Application_Model_Schedule { return $rows; } +/* + "2012-02-23-01-00-00":{ + "row_id":"1", + "id":"caf951f6d8f087c3a90291a9622073f9", + "uri":"http:\/\/localhost:80\/api\/get-media\/file\/caf951f6d8f087c3a90291a9622073f9.mp3", + "fade_in":0, + "fade_out":0, + "cue_in":0, + "cue_out":199.798, + "start":"2012-02-23-01-00-00", + "end":"2012-02-23-01-03-19" + } + * */ public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null){ @@ -488,10 +501,38 @@ class Application_Model_Schedule { } // Scheduler wants everything in a playlist - $data = Application_Model_Schedule::GetItems($range_start, $range_end); + $items = Application_Model_Schedule::GetItems($range_start, $range_end); - Logging::log(print_r($data, true)); + $data = array(); + $utcTimeZone = new DateTimeZone("UTC"); + foreach ($items as $item){ + + $storedFile = Application_Model_StoredFile::Recall($item["file_id"]); + $uri = $storedFile->getFileUrlUsingConfigAddress(); + + $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone); + $trackEndDateTime = new DateTime($item["ends"], $utcTimeZone); + + if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()){ + $diff = $trackEndDateTime->getTimestamp() - $showEndDateTime->getTimestamp(); + //assuming ends takes cue_out into assumption + $item["cue_out"] = $item["cue_out"] - $diff; + } + + $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); + $data[$starts] = array( + 'id' => $storedFile->getGunid(), + 'uri' => $uri, + 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), + 'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]), + 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), + 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), + 'start' => $starts, + 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) + ); + } + return $data; } @@ -564,7 +605,6 @@ class Application_Model_Schedule { $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); $medias[$starts] = array( - 'row_id' => $item["id"], 'id' => $storedFile->getGunid(), 'uri' => $uri, 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), @@ -572,7 +612,6 @@ class Application_Model_Schedule { 'fade_cross' => 0, 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), - 'export_source' => 'scheduler', 'start' => $starts, 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) ); diff --git a/python_apps/pypo/pypo-cli.py b/python_apps/pypo/pypo-cli.py index 4617a30f2..44b9027e3 100644 --- a/python_apps/pypo/pypo-cli.py +++ b/python_apps/pypo/pypo-cli.py @@ -54,23 +54,16 @@ except Exception, e: class Global: def __init__(self): self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') def selfcheck(self): self.api_client = api_client.api_client_factory(config) return self.api_client.is_server_compatible() - - def set_export_source(self, export_source): - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - self.schedule_file = self.cache_dir + 'schedule.pickle' - self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle" def test_api(self): self.api_client.test() """ - def check_schedule(self, export_source): + def check_schedule(self): logger = logging.getLogger() try: diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 797669c0a..a985e79cc 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -45,7 +45,10 @@ class PypoFetch(Thread): def __init__(self, q): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') + + self.cache_dir = os.path.join(config["cache_dir"], "scheduler") + logger.info("Creating cache directory at %s", self.cache_dir) + self.queue = q self.schedule_data = [] logger = logging.getLogger('fetch') @@ -245,15 +248,6 @@ class PypoFetch(Thread): if(status == "true"): self.api_client.notify_liquidsoap_status("OK", stream_id, str(fake_time)) - - - def set_export_source(self, export_source): - logger = logging.getLogger('fetch') - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - logger.info("Creating cache directory at %s", self.cache_dir) - - def update_liquidsoap_stream_format(self, stream_format): # Push stream metadata to liquidsoap # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!! @@ -294,7 +288,7 @@ class PypoFetch(Thread): to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss) - runs the cleanup routine, to get rid of unused cached files """ - def process_schedule(self, schedule_data, export_source, bootstrapping): + def process_schedule(self, schedule_data, bootstrapping): logger = logging.getLogger('fetch') playlists = schedule_data["playlists"] @@ -310,7 +304,7 @@ class PypoFetch(Thread): self.queue.put(scheduled_data) # cleanup - try: self.cleanup(self.export_source) + try: self.cleanup() except Exception, e: logger.error("%s", e) @@ -392,7 +386,7 @@ class PypoFetch(Thread): fileExt = os.path.splitext(media['uri'])[1] try: - dst = "%s%s/%s%s" % (self.cache_dir, pkey, media['id'], fileExt) + dst = os.path.join(self.cache_dir, pkey, media['id']+fileExt) # download media file self.handle_remote_file(media, dst) @@ -406,8 +400,8 @@ class PypoFetch(Thread): if fsize > 0: pl_entry = \ - 'annotate:export_source="%s",media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ - % (media['export_source'], media['id'], 0, \ + 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ + % (media['id'], 0, \ float(media['fade_in']) / 1000, \ float(media['fade_out']) / 1000, \ float(media['cue_in']), \ @@ -452,7 +446,7 @@ class PypoFetch(Thread): Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR" and deletes them. """ - def cleanup(self, export_source): + def cleanup(self): logger = logging.getLogger('fetch') offset = 3600 * int(config["cache_for"]) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 24f48c7cb..1d7c132c9 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -34,7 +34,6 @@ class PypoPush(Thread): def __init__(self, q): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') self.queue = q self.schedule = dict() @@ -42,11 +41,6 @@ class PypoPush(Thread): self.liquidsoap_state_play = True self.push_ahead = 10 - - def set_export_source(self, export_source): - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle" """ The Push Loop - the push loop periodically checks if there is a playlist @@ -54,7 +48,7 @@ class PypoPush(Thread): If yes, the current liquidsoap playlist gets replaced with the corresponding one, then liquidsoap is asked (via telnet) to reload and immediately play it. """ - def push(self, export_source): + def push(self): logger = logging.getLogger('push') timenow = time.time() From bb300676cf61046ab5fed8ebab73ce2290a1eb57 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 22 Feb 2012 17:29:49 -0500 Subject: [PATCH 03/39] CC-3336: Refactor schedule API used by pypo -initial commit --- .../application/controllers/ApiController.php | 2 + airtime_mvc/application/models/Schedule.php | 184 ++++++++++-------- 2 files changed, 104 insertions(+), 82 deletions(-) diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index f9628a78a..6e8aa2b90 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -276,12 +276,14 @@ class ApiController extends Zend_Controller_Action $api_key = $this->_getParam('api_key'); + /* if(!in_array($api_key, $CC_CONFIG["apiKey"])) { header('HTTP/1.0 401 Unauthorized'); print 'You are not allowed to access this resource. '; exit; } + * */ PEAR::setErrorHandling(PEAR_ERROR_RETURN); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 1c2100d35..2256cf629 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -46,87 +46,6 @@ class Application_Model_Schedule { } - /** - * Returns array indexed by: - * "playlistId"/"playlist_id" (aliases to the same thing) - * "start"/"starts" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn - * "end"/"ends" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn - * "group_id"/"id" (aliases to the same thing) - * "clip_length" (for audio clips this is the length of the audio clip, - * for playlists this is the length of the entire playlist) - * "name" (playlist only) - * "creator" (playlist only) - * "file_id" (audioclip only) - * "count" (number of items in the playlist, always 1 for audioclips. - * Note that playlists with one item will also have count = 1. - * - * @param string $p_fromDateTime - * In the format YYYY-MM-DD HH:MM:SS.nnnnnn - * @param string $p_toDateTime - * In the format YYYY-MM-DD HH:MM:SS.nnnnnn - * @param boolean $p_playlistsOnly - * Retrieve playlists as a single item. - * @return array - * Returns empty array if nothing found - */ - - public static function GetItems($p_currentDateTime, $p_toDateTime, $p_playlistsOnly = true) - { - global $CC_CONFIG, $CC_DBC; - $rows = array(); - if (!$p_playlistsOnly) { - $sql = "SELECT * FROM ".$CC_CONFIG["scheduleTable"] - ." WHERE (starts >= TIMESTAMP '$p_currentDateTime') " - ." AND (ends <= TIMESTAMP '$p_toDateTime')"; - $rows = $CC_DBC->GetAll($sql); - foreach ($rows as &$row) { - $row["count"] = "1"; - $row["playlistId"] = $row["playlist_id"]; - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - $row["id"] = $row["group_id"]; - } - } else { - $sql = "SELECT MIN(pt.creator) AS creator," - ." st.group_id," - ." SUM(st.clip_length) AS clip_length," - ." MIN(st.file_id) AS file_id," - ." COUNT(*) as count," - ." MIN(st.playlist_id) AS playlist_id," - ." MIN(st.starts) AS starts," - ." MAX(st.ends) AS ends," - ." MIN(sh.name) AS show_name," - ." MIN(si.starts) AS show_start," - ." MAX(si.ends) AS show_end" - ." FROM $CC_CONFIG[scheduleTable] as st" - ." LEFT JOIN $CC_CONFIG[playListTable] as pt" - ." ON st.playlist_id = pt.id" - ." LEFT JOIN $CC_CONFIG[showInstances] as si" - ." ON st.instance_id = si.id" - ." LEFT JOIN $CC_CONFIG[showTable] as sh" - ." ON si.show_id = sh.id" - //The next line ensures we only get songs that haven't ended yet - ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" - ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" - //next line makes sure that we aren't returning items that - //are past the show's scheduled timeslot. - ." AND (st.starts < si.ends)" - ." GROUP BY st.group_id" - ." ORDER BY starts"; - - $rows = $CC_DBC->GetAll($sql); - if (!PEAR::isError($rows)) { - foreach ($rows as &$row) { - $row["playlistId"] = $row["playlist_id"]; - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - $row["id"] = $row["group_id"]; - } - } - } - return $rows; - } - /** * Returns data related to the scheduled items. * @@ -473,7 +392,108 @@ class Application_Model_Schedule { } return $diff; } + + /** + * Returns array indexed by: + * "playlistId"/"playlist_id" (aliases to the same thing) + * "start"/"starts" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn + * "end"/"ends" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn + * "group_id"/"id" (aliases to the same thing) + * "clip_length" (for audio clips this is the length of the audio clip, + * for playlists this is the length of the entire playlist) + * "name" (playlist only) + * "creator" (playlist only) + * "file_id" (audioclip only) + * "count" (number of items in the playlist, always 1 for audioclips. + * Note that playlists with one item will also have count = 1. + * + * @param string $p_fromDateTime + * In the format YYYY-MM-DD HH:MM:SS.nnnnnn + * @param string $p_toDateTime + * In the format YYYY-MM-DD HH:MM:SS.nnnnnn + * @param boolean $p_playlistsOnly + * Retrieve playlists as a single item. + * @return array + * Returns null if nothing found + */ + public static function GetItems($p_currentDateTime, $p_toDateTime) + { + global $CC_CONFIG, $CC_DBC; + $rows = array(); + + $sql = "SELECT st.file_id AS file_id," + ." st.starts AS starts," + ." st.ends AS ends," + ." si.starts as show_start," + ." si.ends as show_end" + ." FROM $CC_CONFIG[scheduleTable] as st" + ." LEFT JOIN $CC_CONFIG[showInstances] as si" + ." ON st.instance_id = si.id" + ." ORDER BY starts"; + + + /* + $sql = "SELECT pt.creator as creator" + ." st.file_id AS file_id" + ." st.starts AS starts" + ." st.ends AS ends" + ." st.name as show_name" + ." si.starts as show_start" + ." si.ends as show_end" + ." FROM $CC_CONFIG[scheduleTable] as st" + ." LEFT JOIN $CC_CONFIG[showInstances] as si" + ." ON st.instance_id = si.id" + ." LEFT JOIN $CC_CONFIG[showTable] as sh" + ." ON si.show_id = sh.id" + //The next line ensures we only get songs that haven't ended yet + ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" + ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" + //next line makes sure that we aren't returning items that + //are past the show's scheduled timeslot. + ." AND (st.starts < si.ends)" + ." ORDER BY starts"; + * */ + + $rows = $CC_DBC->GetAll($sql); + if (!PEAR::isError($rows)) { + foreach ($rows as &$row) { + $row["start"] = $row["starts"]; + $row["end"] = $row["ends"]; + } + } + + return $rows; + } + + + public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null){ + + global $CC_CONFIG, $CC_DBC; + + /* 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()); + $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()); + $t2->add(new DateInterval("PT24H")); + $range_end = $t2->format("Y-m-d H:i:s"); + } else { + $range_end = Application_Model_Schedule::PypoTimeToAirtimeTime($p_toDateTime); + } + + // Scheduler wants everything in a playlist + $data = Application_Model_Schedule::GetItems($range_start, $range_end); + + Logging::log(print_r($data, true)); + + return $data; + } /** * Export the schedule in json formatted for pypo (the liquidsoap scheduler) @@ -483,7 +503,7 @@ class Application_Model_Schedule { * @param string $p_toDateTime * In the format "YYYY-MM-DD-HH-mm-SS" */ - public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null) + public static function GetScheduledPlaylistsOld($p_fromDateTime = null, $p_toDateTime = null) { global $CC_CONFIG, $CC_DBC; From c433b0815838fca9eb2f03316d3396239fc7b4df Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 22 Feb 2012 20:41:24 -0500 Subject: [PATCH 04/39] CC-3336: Refactor schedule API used by pypo -removed export_source -rewrote GetScheduledPlaylists() --- airtime_mvc/application/models/Schedule.php | 47 +++++++++++++++++++-- python_apps/pypo/pypo-cli.py | 9 +--- python_apps/pypo/pypofetch.py | 26 +++++------- python_apps/pypo/pypopush.py | 8 +--- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 2256cf629..40453c1b5 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -466,6 +466,19 @@ class Application_Model_Schedule { return $rows; } +/* + "2012-02-23-01-00-00":{ + "row_id":"1", + "id":"caf951f6d8f087c3a90291a9622073f9", + "uri":"http:\/\/localhost:80\/api\/get-media\/file\/caf951f6d8f087c3a90291a9622073f9.mp3", + "fade_in":0, + "fade_out":0, + "cue_in":0, + "cue_out":199.798, + "start":"2012-02-23-01-00-00", + "end":"2012-02-23-01-03-19" + } + * */ public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null){ @@ -488,10 +501,38 @@ class Application_Model_Schedule { } // Scheduler wants everything in a playlist - $data = Application_Model_Schedule::GetItems($range_start, $range_end); + $items = Application_Model_Schedule::GetItems($range_start, $range_end); - Logging::log(print_r($data, true)); + $data = array(); + $utcTimeZone = new DateTimeZone("UTC"); + foreach ($items as $item){ + + $storedFile = Application_Model_StoredFile::Recall($item["file_id"]); + $uri = $storedFile->getFileUrlUsingConfigAddress(); + + $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone); + $trackEndDateTime = new DateTime($item["ends"], $utcTimeZone); + + if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()){ + $diff = $trackEndDateTime->getTimestamp() - $showEndDateTime->getTimestamp(); + //assuming ends takes cue_out into assumption + $item["cue_out"] = $item["cue_out"] - $diff; + } + + $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); + $data[$starts] = array( + 'id' => $storedFile->getGunid(), + 'uri' => $uri, + 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), + 'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]), + 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), + 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), + 'start' => $starts, + 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) + ); + } + return $data; } @@ -564,7 +605,6 @@ class Application_Model_Schedule { $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); $medias[$starts] = array( - 'row_id' => $item["id"], 'id' => $storedFile->getGunid(), 'uri' => $uri, 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), @@ -572,7 +612,6 @@ class Application_Model_Schedule { 'fade_cross' => 0, 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), - 'export_source' => 'scheduler', 'start' => $starts, 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) ); diff --git a/python_apps/pypo/pypo-cli.py b/python_apps/pypo/pypo-cli.py index 4617a30f2..44b9027e3 100644 --- a/python_apps/pypo/pypo-cli.py +++ b/python_apps/pypo/pypo-cli.py @@ -54,23 +54,16 @@ except Exception, e: class Global: def __init__(self): self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') def selfcheck(self): self.api_client = api_client.api_client_factory(config) return self.api_client.is_server_compatible() - - def set_export_source(self, export_source): - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - self.schedule_file = self.cache_dir + 'schedule.pickle' - self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle" def test_api(self): self.api_client.test() """ - def check_schedule(self, export_source): + def check_schedule(self): logger = logging.getLogger() try: diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 797669c0a..a985e79cc 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -45,7 +45,10 @@ class PypoFetch(Thread): def __init__(self, q): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') + + self.cache_dir = os.path.join(config["cache_dir"], "scheduler") + logger.info("Creating cache directory at %s", self.cache_dir) + self.queue = q self.schedule_data = [] logger = logging.getLogger('fetch') @@ -245,15 +248,6 @@ class PypoFetch(Thread): if(status == "true"): self.api_client.notify_liquidsoap_status("OK", stream_id, str(fake_time)) - - - def set_export_source(self, export_source): - logger = logging.getLogger('fetch') - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - logger.info("Creating cache directory at %s", self.cache_dir) - - def update_liquidsoap_stream_format(self, stream_format): # Push stream metadata to liquidsoap # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!! @@ -294,7 +288,7 @@ class PypoFetch(Thread): to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss) - runs the cleanup routine, to get rid of unused cached files """ - def process_schedule(self, schedule_data, export_source, bootstrapping): + def process_schedule(self, schedule_data, bootstrapping): logger = logging.getLogger('fetch') playlists = schedule_data["playlists"] @@ -310,7 +304,7 @@ class PypoFetch(Thread): self.queue.put(scheduled_data) # cleanup - try: self.cleanup(self.export_source) + try: self.cleanup() except Exception, e: logger.error("%s", e) @@ -392,7 +386,7 @@ class PypoFetch(Thread): fileExt = os.path.splitext(media['uri'])[1] try: - dst = "%s%s/%s%s" % (self.cache_dir, pkey, media['id'], fileExt) + dst = os.path.join(self.cache_dir, pkey, media['id']+fileExt) # download media file self.handle_remote_file(media, dst) @@ -406,8 +400,8 @@ class PypoFetch(Thread): if fsize > 0: pl_entry = \ - 'annotate:export_source="%s",media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ - % (media['export_source'], media['id'], 0, \ + 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ + % (media['id'], 0, \ float(media['fade_in']) / 1000, \ float(media['fade_out']) / 1000, \ float(media['cue_in']), \ @@ -452,7 +446,7 @@ class PypoFetch(Thread): Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR" and deletes them. """ - def cleanup(self, export_source): + def cleanup(self): logger = logging.getLogger('fetch') offset = 3600 * int(config["cache_for"]) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 24f48c7cb..1d7c132c9 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -34,7 +34,6 @@ class PypoPush(Thread): def __init__(self, q): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') self.queue = q self.schedule = dict() @@ -42,11 +41,6 @@ class PypoPush(Thread): self.liquidsoap_state_play = True self.push_ahead = 10 - - def set_export_source(self, export_source): - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle" """ The Push Loop - the push loop periodically checks if there is a playlist @@ -54,7 +48,7 @@ class PypoPush(Thread): If yes, the current liquidsoap playlist gets replaced with the corresponding one, then liquidsoap is asked (via telnet) to reload and immediately play it. """ - def push(self, export_source): + def push(self): logger = logging.getLogger('push') timenow = time.time() From 4631e199cc3c0bb6f915a20cd550d343f3a87539 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Thu, 23 Feb 2012 11:21:16 -0500 Subject: [PATCH 05/39] CC-3336: Refactor schedule API used by pypo -add comment --- airtime_mvc/application/models/Schedule.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 40453c1b5..536655c57 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -514,6 +514,9 @@ class Application_Model_Schedule { $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone); $trackEndDateTime = new DateTime($item["ends"], $utcTimeZone); + /* Note: cue_out and end are always the same. */ + /* TODO: Not all tracks will have "show_end" */ + if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()){ $diff = $trackEndDateTime->getTimestamp() - $showEndDateTime->getTimestamp(); //assuming ends takes cue_out into assumption From e8f329aef1276a70de812fb15c3409bd6133287d Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Mon, 27 Feb 2012 13:52:35 -0500 Subject: [PATCH 06/39] CC-3336: Refactor schedule API used by pypo --- airtime_mvc/application/models/Schedule.php | 53 +-- python_apps/api_clients/api_client.py | 6 +- python_apps/pypo/pypofetch.py | 348 ++++++++++---------- python_apps/pypo/pypopush.py | 94 +++--- 4 files changed, 229 insertions(+), 272 deletions(-) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 536655c57..513230c5c 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -423,46 +423,22 @@ class Application_Model_Schedule { $rows = array(); $sql = "SELECT st.file_id AS file_id," - ." st.starts AS starts," - ." st.ends AS ends," + ." st.starts AS start," + ." st.ends AS end," ." si.starts as show_start," ." si.ends as show_end" ." FROM $CC_CONFIG[scheduleTable] as st" ." LEFT JOIN $CC_CONFIG[showInstances] as si" ." ON st.instance_id = si.id" - ." ORDER BY starts"; - - - /* - $sql = "SELECT pt.creator as creator" - ." st.file_id AS file_id" - ." st.starts AS starts" - ." st.ends AS ends" - ." st.name as show_name" - ." si.starts as show_start" - ." si.ends as show_end" - ." FROM $CC_CONFIG[scheduleTable] as st" - ." LEFT JOIN $CC_CONFIG[showInstances] as si" - ." ON st.instance_id = si.id" - ." LEFT JOIN $CC_CONFIG[showTable] as sh" - ." ON si.show_id = sh.id" - //The next line ensures we only get songs that haven't ended yet - ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" - ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" - //next line makes sure that we aren't returning items that - //are past the show's scheduled timeslot. - ." AND (st.starts < si.ends)" - ." ORDER BY starts"; - * */ + ." ORDER BY start"; + Logging::log($sql); + $rows = $CC_DBC->GetAll($sql); - if (!PEAR::isError($rows)) { - foreach ($rows as &$row) { - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - } + if (PEAR::isError($rows)) { + return null; } - + return $rows; } @@ -506,13 +482,16 @@ class Application_Model_Schedule { $data = array(); $utcTimeZone = new DateTimeZone("UTC"); + $data["status"] = array(); + $data["media"] = array(); + foreach ($items as $item){ $storedFile = Application_Model_StoredFile::Recall($item["file_id"]); $uri = $storedFile->getFileUrlUsingConfigAddress(); $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone); - $trackEndDateTime = new DateTime($item["ends"], $utcTimeZone); + $trackEndDateTime = new DateTime($item["end"], $utcTimeZone); /* Note: cue_out and end are always the same. */ /* TODO: Not all tracks will have "show_end" */ @@ -523,16 +502,16 @@ class Application_Model_Schedule { $item["cue_out"] = $item["cue_out"] - $diff; } - $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); - $data[$starts] = array( + $start = Application_Model_Schedule::AirtimeTimeToPypoTime($item["start"]); + $data["media"][$start] = array( 'id' => $storedFile->getGunid(), 'uri' => $uri, 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), 'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]), 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), - 'start' => $starts, - 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) + 'start' => $start, + 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["end"]) ); } diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py index c6bd1763f..e33d612f0 100755 --- a/python_apps/api_clients/api_client.py +++ b/python_apps/api_clients/api_client.py @@ -255,15 +255,15 @@ class AirTimeApiClient(ApiClientInterface): export_url = export_url.replace('%%api_key%%', self.config["api_key"]) response = "" - status = 0 try: response_json = self.get_response_from_server(export_url) response = json.loads(response_json) - status = response['check'] + success = True except Exception, e: logger.error(e) + success = False - return status, response + return success, response def get_media(self, uri, dst): diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index a985e79cc..6919a78f2 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -46,17 +46,17 @@ class PypoFetch(Thread): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) + self.logger = logging.getLogger(); + self.cache_dir = os.path.join(config["cache_dir"], "scheduler") - logger.info("Creating cache directory at %s", self.cache_dir) + self.logger.info("Creating cache directory at %s", self.cache_dir) self.queue = q self.schedule_data = [] - logger = logging.getLogger('fetch') - logger.info("PypoFetch: init complete") + self.logger.info("PypoFetch: init complete") def init_rabbit_mq(self): - logger = logging.getLogger('fetch') - logger.info("Initializing RabbitMQ stuff") + self.logger.info("Initializing RabbitMQ stuff") try: schedule_exchange = Exchange("airtime-pypo", "direct", durable=True, auto_delete=True) schedule_queue = Queue("pypo-fetch", exchange=schedule_exchange, key="foo") @@ -64,7 +64,7 @@ class PypoFetch(Thread): channel = connection.channel() self.simple_queue = SimpleQueue(channel, schedule_queue) except Exception, e: - logger.error(e) + self.logger.error(e) return False return True @@ -75,49 +75,46 @@ class PypoFetch(Thread): """ def handle_message(self, message): try: - logger = logging.getLogger('fetch') - logger.info("Received event from RabbitMQ: %s" % message) + self.logger.info("Received event from RabbitMQ: %s" % message) m = json.loads(message) command = m['event_type'] - logger.info("Handling command: " + command) + self.logger.info("Handling command: " + command) if command == 'update_schedule': self.schedule_data = m['schedule'] - self.process_schedule(self.schedule_data, "scheduler", False) + self.process_schedule(self.schedule_data, False) elif command == 'update_stream_setting': - logger.info("Updating stream setting...") + self.logger.info("Updating stream setting...") self.regenerateLiquidsoapConf(m['setting']) elif command == 'update_stream_format': - logger.info("Updating stream format...") + self.logger.info("Updating stream format...") self.update_liquidsoap_stream_format(m['stream_format']) elif command == 'update_station_name': - logger.info("Updating station name...") + self.logger.info("Updating station name...") self.update_liquidsoap_station_name(m['station_name']) elif command == 'cancel_current_show': - logger.info("Cancel current show command received...") + self.logger.info("Cancel current show command received...") self.stop_current_show() except Exception, e: - logger.error("Exception in handling RabbitMQ message: %s", e) + self.logger.error("Exception in handling RabbitMQ message: %s", e) def stop_current_show(self): - logger = logging.getLogger('fetch') - logger.debug('Notifying Liquidsoap to stop playback.') + self.logger.debug('Notifying Liquidsoap to stop playback.') try: tn = telnetlib.Telnet(LS_HOST, LS_PORT) tn.write('source.skip\n') tn.write('exit\n') tn.read_all() except Exception, e: - logger.debug(e) - logger.debug('Could not connect to liquidsoap') + self.logger.debug(e) + self.logger.debug('Could not connect to liquidsoap') def regenerateLiquidsoapConf(self, setting): - logger = logging.getLogger('fetch') existing = {} # create a temp file fh = open('/etc/airtime/liquidsoap.cfg', 'r') - logger.info("Reading existing config...") + self.logger.info("Reading existing config...") # read existing conf file and build dict while 1: line = fh.readline() @@ -147,7 +144,7 @@ class PypoFetch(Thread): #restart flag restart = False - logger.info("Looking for changes...") + self.logger.info("Looking for changes...") # look for changes for s in setting: if "output_sound_device" in s[u'keyname'] or "icecast_vorbis_metadata" in s[u'keyname']: @@ -155,13 +152,13 @@ class PypoFetch(Thread): state_change_restart[stream] = False # This is the case where restart is required no matter what if (existing[s[u'keyname']] != s[u'value']): - logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) + self.logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) restart = True; else: stream, dump = s[u'keyname'].split('_',1) if "_output" in s[u'keyname']: if (existing[s[u'keyname']] != s[u'value']): - logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) + self.logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) restart = True; state_change_restart[stream] = True elif ( s[u'value'] != 'disabled'): @@ -173,22 +170,22 @@ class PypoFetch(Thread): if stream not in change: change[stream] = False if not (s[u'value'] == existing[s[u'keyname']]): - logger.info("Keyname: %s, Curent value: %s, New Value: %s", s[u'keyname'], existing[s[u'keyname']], s[u'value']) + self.logger.info("Keyname: %s, Curent value: %s, New Value: %s", s[u'keyname'], existing[s[u'keyname']], s[u'value']) change[stream] = True # set flag change for sound_device alway True - logger.info("Change:%s, State_Change:%s...", change, state_change_restart) + self.logger.info("Change:%s, State_Change:%s...", change, state_change_restart) for k, v in state_change_restart.items(): if k == "sound_device" and v: restart = True elif v and change[k]: - logger.info("'Need-to-restart' state detected for %s...", k) + self.logger.info("'Need-to-restart' state detected for %s...", k) restart = True # rewrite if restart: fh = open('/etc/airtime/liquidsoap.cfg', 'w') - logger.info("Rewriting liquidsoap.cfg...") + self.logger.info("Rewriting liquidsoap.cfg...") fh.write("################################################\n") fh.write("# THIS FILE IS AUTO GENERATED. DO NOT CHANGE!! #\n") fh.write("################################################\n") @@ -210,17 +207,16 @@ class PypoFetch(Thread): fh.close() # restarting pypo. # we could just restart liquidsoap but it take more time somehow. - logger.info("Restarting pypo...") + self.logger.info("Restarting pypo...") sys.exit(0) else: - logger.info("No change detected in setting...") + self.logger.info("No change detected in setting...") self.update_liquidsoap_connection_status() """ updates the status of liquidsoap connection to the streaming server This fucntion updates the bootup time variable in liquidsoap script """ def update_liquidsoap_connection_status(self): - logger = logging.getLogger('fetch') tn = telnetlib.Telnet(LS_HOST, LS_PORT) # update the boot up time of liquidsoap. Since liquidsoap is not restarting, # we are manually adjusting the bootup time variable so the status msg will get @@ -238,7 +234,7 @@ class PypoFetch(Thread): # streamin info is in the form of: # eg. s1:true,2:true,3:false streams = stream_info.split(",") - logger.info(streams) + self.logger.info(streams) fake_time = current_time + 1 for s in streams: @@ -252,33 +248,31 @@ class PypoFetch(Thread): # Push stream metadata to liquidsoap # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!! try: - logger = logging.getLogger('fetch') - logger.info(LS_HOST) - logger.info(LS_PORT) + self.logger.info(LS_HOST) + self.logger.info(LS_PORT) tn = telnetlib.Telnet(LS_HOST, LS_PORT) command = ('vars.stream_metadata_type %s\n' % stream_format).encode('utf-8') - logger.info(command) + self.logger.info(command) tn.write(command) tn.write('exit\n') tn.read_all() except Exception, e: - logger.error("Exception %s", e) + self.logger.error("Exception %s", e) def update_liquidsoap_station_name(self, station_name): # Push stream metadata to liquidsoap # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!! try: - logger = logging.getLogger('fetch') - logger.info(LS_HOST) - logger.info(LS_PORT) + self.logger.info(LS_HOST) + self.logger.info(LS_PORT) tn = telnetlib.Telnet(LS_HOST, LS_PORT) command = ('vars.station_name %s\n' % station_name).encode('utf-8') - logger.info(command) + self.logger.info(command) tn.write(command) tn.write('exit\n') tn.read_all() except Exception, e: - logger.error("Exception %s", e) + self.logger.error("Exception %s", e) """ Process the schedule @@ -289,166 +283,161 @@ class PypoFetch(Thread): - runs the cleanup routine, to get rid of unused cached files """ def process_schedule(self, schedule_data, bootstrapping): - logger = logging.getLogger('fetch') - playlists = schedule_data["playlists"] + media = schedule_data["media"] # Download all the media and put playlists in liquidsoap "annotate" format try: - liquidsoap_playlists = self.prepare_playlists(playlists, bootstrapping) - except Exception, e: logger.error("%s", e) + media = self.prepare_media(media, bootstrapping) + except Exception, e: self.logger.error("%s", e) # Send the data to pypo-push scheduled_data = dict() - scheduled_data['liquidsoap_playlists'] = liquidsoap_playlists - scheduled_data['schedule'] = playlists - self.queue.put(scheduled_data) + scheduled_data['liquidsoap_annotation_queue'] = liquidsoap_annotation_queue + self.queue.put(media) + """ # cleanup try: self.cleanup() - except Exception, e: logger.error("%s", e) + except Exception, e: self.logger.error("%s", e) + """ + - """ - In this function every audio file is cut as necessary (cue_in/cue_out != 0) - and stored in a playlist folder. - file is e.g. 2010-06-23-15-00-00/17_cue_10.132-123.321.mp3 - """ - def prepare_playlists(self, playlists, bootstrapping): - logger = logging.getLogger('fetch') - - liquidsoap_playlists = dict() - - # Dont do anything if playlists is empty - if not playlists: - logger.debug("Schedule is empty.") - return liquidsoap_playlists - - scheduleKeys = sorted(playlists.iterkeys()) - + def prepare_media(self, media, bootstrapping): + """ + Iterate through the list of media items in "media" and + download them. + """ try: - for pkey in scheduleKeys: - logger.info("Playlist starting at %s", pkey) - playlist = playlists[pkey] + mediaKeys = sorted(media.iterkeys()) + for mkey in mediaKeys: + self.logger.debug("Media item starting at %s", mkey) + media_item = media[mkey] + + if bootstrapping: + check_for_crash(media_item) # create playlist directory try: - os.mkdir(self.cache_dir + str(pkey)) + """ + Extract year, month, date from mkey + """ + y_m_d = mkey[0:10] + download_dir = os.mkdir(os.path.join(self.cache_dir, y_m_d)) + fileExt = os.path.splitext(media_item['uri'])[1] + dst = os.path.join(download_dir, media_item['id']+fileExt) except Exception, e: - logger.warning(e) + self.logger.warning(e) + + if self.handle_media_file(media_item, dst): + entry = create_liquidsoap_annotation(media_item, dst) + #entry['show_name'] = playlist['show_name'] + entry['show_name'] = "TODO" + media_item["annotation"] = entry - ls_playlist = self.handle_media_file(playlist, pkey, bootstrapping) - - liquidsoap_playlists[pkey] = ls_playlist except Exception, e: - logger.error("%s", e) - return liquidsoap_playlists + self.logger.error("%s", e) + + return media + + def create_liquidsoap_annotation(media, dst): + pl_entry = \ + 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ + % (media['id'], 0, \ + float(media['fade_in']) / 1000, \ + float(media['fade_out']) / 1000, \ + float(media['cue_in']), \ + float(media['cue_out']), \ + media['row_id'], dst) - """ - Download and cache the media files. - This handles both remote and local files. - Returns an updated ls_playlist string. - """ - def handle_media_file(self, playlist, pkey, bootstrapping): - logger = logging.getLogger('fetch') + """ + Tracks are only added to the playlist if they are accessible + on the file system and larger than 0 bytes. + So this can lead to playlists shorter than expectet. + (there is a hardware silence detector for this cases...) + """ + entry = dict() + entry['type'] = 'file' + entry['annotate'] = pl_entry + return entry - ls_playlist = [] + def check_for_crash(media_item): + start = media_item['start'] + end = media_item['end'] dtnow = datetime.utcnow() str_tnow_s = dtnow.strftime('%Y-%m-%d-%H-%M-%S') - - sortedKeys = sorted(playlist['medias'].iterkeys()) - - for key in sortedKeys: - media = playlist['medias'][key] - logger.debug("Processing track %s", media['uri']) + + if start <= str_tnow_s and str_tnow_s < end: + #song is currently playing and we just started pypo. Maybe there + #was a power outage? Let's restart playback of this song. + start_split = map(int, start.split('-')) + media_start = datetime(start_split[0], start_split[1], start_split[2], start_split[3], start_split[4], start_split[5], 0, None) + self.logger.debug("Found media item that started at %s.", media_start) - if bootstrapping: - start = media['start'] - end = media['end'] - - if end <= str_tnow_s: - continue - elif start <= str_tnow_s and str_tnow_s < end: - #song is currently playing and we just started pypo. Maybe there - #was a power outage? Let's restart playback of this song. - start_split = map(int, start.split('-')) - media_start = datetime(start_split[0], start_split[1], start_split[2], start_split[3], start_split[4], start_split[5], 0, None) - logger.debug("Found media item that started at %s.", media_start) - - delta = dtnow - media_start #we get a TimeDelta object from this operation - logger.info("Starting media item at %d second point", delta.seconds) - media['cue_in'] = delta.seconds + 10 - td = timedelta(seconds=10) - playlist['start'] = (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S') - logger.info("Crash detected, setting playlist to restart at %s", (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S')) + delta = dtnow - media_start #we get a TimeDelta object from this operation + self.logger.info("Starting media item at %d second point", delta.seconds) + """ + Set the cue_in. This is used by Liquidsoap to determine at what point in the media + item it should start playing. If the cue_in happens to be > cue_out, then make cue_in = cue_out + """ + media_item['cue_in'] = delta.seconds + 10 if delta.seconds + 10 < media_item['cue_out'] else media_item['cue_out'] + + """ + Set the start time, which is used by pypo-push to determine when a media item is scheduled. + Pushing the start time into the future will ensure pypo-push will push this to Liquidsoap. + """ + td = timedelta(seconds=10) + media_item['start'] = (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S') + self.logger.info("Crash detected, setting playlist to restart at %s", (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S')) + + def handle_media_file(self, media_item, dst): + """ + Download and cache the media item. + """ + + self.logger.debug("Processing track %s", media_item['uri']) - fileExt = os.path.splitext(media['uri'])[1] - try: - dst = os.path.join(self.cache_dir, pkey, media['id']+fileExt) - - # download media file - self.handle_remote_file(media, dst) - - if True == os.access(dst, os.R_OK): - # check filesize (avoid zero-byte files) - try: fsize = os.path.getsize(dst) - except Exception, e: - logger.error("%s", e) - fsize = 0 - + try: + #blocking function to download the media item + self.download_file(media_item, dst) + + if os.access(dst, os.R_OK): + # check filesize (avoid zero-byte files) + try: + fsize = os.path.getsize(dst) if fsize > 0: - pl_entry = \ - 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ - % (media['id'], 0, \ - float(media['fade_in']) / 1000, \ - float(media['fade_out']) / 1000, \ - float(media['cue_in']), \ - float(media['cue_out']), \ - media['row_id'], dst) + return True + except Exception, e: + self.logger.error("%s", e) + fsize = 0 + else: + self.logger.warning("Cannot read file %s.", dst) - """ - Tracks are only added to the playlist if they are accessible - on the file system and larger than 0 bytes. - So this can lead to playlists shorter than expectet. - (there is a hardware silence detector for this cases...) - """ - entry = dict() - entry['type'] = 'file' - entry['annotate'] = pl_entry - entry['show_name'] = playlist['show_name'] - ls_playlist.append(entry) - - else: - logger.warning("zero-size file - skipping %s. will not add it to playlist at %s", media['uri'], dst) - - else: - logger.warning("something went wrong. file %s not available. will not add it to playlist", dst) - - except Exception, e: logger.info("%s", e) - return ls_playlist + except Exception, e: + self.logger.info("%s", e) + + return False """ Download a file from a remote server and store it in the cache. """ - def handle_remote_file(self, media, dst): - logger = logging.getLogger('fetch') + def download_file(self, media_item, dst): if os.path.isfile(dst): pass - #logger.debug("file already in cache: %s", dst) + #self.logger.debug("file already in cache: %s", dst) else: - logger.debug("try to download %s", media['uri']) - self.api_client.get_media(media['uri'], dst) + self.logger.debug("try to download %s", media_item['uri']) + self.api_client.get_media(media_item['uri'], dst) """ Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR" and deletes them. """ def cleanup(self): - logger = logging.getLogger('fetch') - offset = 3600 * int(config["cache_for"]) now = time.time() @@ -458,39 +447,40 @@ class PypoFetch(Thread): timestamp = calendar.timegm(time.strptime(dir, "%Y-%m-%d-%H-%M-%S")) if (now - timestamp) > offset: try: - logger.debug('trying to remove %s - timestamp: %s', os.path.join(r, dir), timestamp) + self.logger.debug('trying to remove %s - timestamp: %s', os.path.join(r, dir), timestamp) shutil.rmtree(os.path.join(r, dir)) except Exception, e: - logger.error("%s", e) + self.logger.error("%s", e) pass else: - logger.info('sucessfully removed %s', os.path.join(r, dir)) + self.logger.info('sucessfully removed %s', os.path.join(r, dir)) except Exception, e: - logger.error(e) + self.logger.error(e) def main(self): - logger = logging.getLogger('fetch') - try: os.mkdir(self.cache_dir) except Exception, e: pass - # Bootstrap: since we are just starting up, we need to grab the - # most recent schedule. After that we can just wait for updates. - status, self.schedule_data = self.api_client.get_schedule() - if status == 1: - logger.info("Bootstrap schedule received: %s", self.schedule_data) - self.process_schedule(self.schedule_data, "scheduler", True) - logger.info("Bootstrap complete: got initial copy of the schedule") + try: + # Bootstrap: since we are just starting up, we need to grab the + # most recent schedule. After that we can just wait for updates. + success, self.schedule_data = self.api_client.get_schedule() + if success: + self.logger.info("Bootstrap schedule received: %s", self.schedule_data) + self.process_schedule(self.schedule_data, True) + self.logger.info("Bootstrap complete: got initial copy of the schedule") - while not self.init_rabbit_mq(): - logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds") - time.sleep(5) + while not self.init_rabbit_mq(): + self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds") + time.sleep(5) + except Exception, e: + self.logger.error(str(e)) loops = 1 while True: - logger.info("Loop #%s", loops) + self.logger.info("Loop #%s", loops) try: try: message = self.simple_queue.get(block=True) @@ -498,17 +488,17 @@ class PypoFetch(Thread): # ACK the message to take it off the queue message.ack() except MessageStateError, m: - logger.error("Message ACK error: %s", m) + self.logger.error("Message ACK error: %s", m) except Exception, e: """ There is a problem with the RabbitMq messenger service. Let's log the error and get the schedule via HTTP polling """ - logger.error("Exception, %s", e) + self.logger.error("Exception, %s", e) status, self.schedule_data = self.api_client.get_schedule() if status == 1: - self.process_schedule(self.schedule_data, "scheduler", False) + self.process_schedule(self.schedule_data, False) loops += 1 diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 1d7c132c9..2ecdbbe6e 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -36,11 +36,10 @@ class PypoPush(Thread): self.api_client = api_client.api_client_factory(config) self.queue = q - self.schedule = dict() - self.playlists = dict() + self.media = dict() self.liquidsoap_state_play = True - self.push_ahead = 10 + self.push_ahead = 30 """ The Push Loop - the push loop periodically checks if there is a playlist @@ -56,35 +55,30 @@ class PypoPush(Thread): if not self.queue.empty(): # make sure we get the latest schedule while not self.queue.empty(): - scheduled_data = self.queue.get() - logger.debug("Received data from pypo-fetch") - self.schedule = scheduled_data['schedule'] - self.playlists = scheduled_data['liquidsoap_playlists'] - - logger.debug('schedule %s' % json.dumps(self.schedule)) - logger.debug('playlists %s' % json.dumps(self.playlists)) + self.media = self.queue.get() + logger.debug("Received data from pypo-fetch") + logger.debug('media %s' % json.dumps(self.media)) - schedule = self.schedule - playlists = self.playlists + media = self.media currently_on_air = False - if schedule: + if media: tnow = time.gmtime(timenow) tcoming = time.gmtime(timenow + self.push_ahead) str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5]) str_tcoming_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcoming[0], tcoming[1], tcoming[2], tcoming[3], tcoming[4], tcoming[5]) - for pkey in schedule: - plstart = schedule[pkey]['start'][0:19] - - if str_tnow_s <= plstart and plstart < str_tcoming_s: - logger.debug('Preparing to push playlist scheduled at: %s', pkey) - playlist = schedule[pkey] - - - # We have a match, replace the current playlist and - # force liquidsoap to refresh. - if (self.push_liquidsoap(pkey, schedule, playlists) == 1): + + for media_item in media: + item_start = media_item['start'][0:19] + + if str_tnow_s <= item_start and item_start < str_tcoming_s: + """ + If the media item starts in the next 30 seconds, push it to the queue. + """ + logger.debug('Preparing to push media item scheduled at: %s', pkey) + + if self.push_to_liquidsoap(media_item): logger.debug("Pushed to liquidsoap, updating 'played' status.") currently_on_air = True @@ -93,33 +87,31 @@ class PypoPush(Thread): # Call API to update schedule states logger.debug("Doing callback to server to update 'played' status.") self.api_client.notify_scheduled_item_start_playing(pkey, schedule) + - show_start = schedule[pkey]['show_start'] - show_end = schedule[pkey]['show_end'] - - if show_start <= str_tnow_s and str_tnow_s < show_end: - currently_on_air = True + def push_to_liquidsoap(self, media_item): + if media_item["starts"] == self.last_end_time: """ - If currently_on_air = False but liquidsoap_state_play = True then it means that Liquidsoap may - still be playing audio even though the show has ended ('currently_on_air = False' means no show is scheduled) - See CC-3231. - This is a temporary solution for Airtime 2.0 - """ - if not currently_on_air and self.liquidsoap_state_play: - logger.debug('Notifying Liquidsoap to stop playback.') - try: - tn = telnetlib.Telnet(LS_HOST, LS_PORT) - tn.write('source.skip\n') - tn.write('exit\n') - tn.read_all() - except Exception, e: - logger.debug(e) - logger.debug('Could not connect to liquidsoap') + this media item is attached to the end of the last + track, so let's push it now so that Liquidsoap can start playing + it immediately after (and prepare crossfades if need be). + """ + tn = telnetlib.Telnet(LS_HOST, LS_PORT) + tn.write(str('queue.push %s\n' % media_item["annotation"].encode('utf-8'))) + #TODO: vars.pypo_data + #TODO: vars.show_name + tn.write("exit\n") + + self.last_end_time = media_item["end"] + else: + """ + this media item does not start right after a current playing track. + We need to sleep, and then wake up when this track starts. + """ + + return False - self.liquidsoap_state_play = False - - - def push_liquidsoap(self, pkey, schedule, playlists): + def push_liquidsoap_old(self, pkey, schedule, playlists): logger = logging.getLogger('push') try: @@ -127,10 +119,6 @@ class PypoPush(Thread): plstart = schedule[pkey]['start'][0:19] #strptime returns struct_time in local time - #mktime takes a time_struct and returns a floating point - #gmtime Convert a time expressed in seconds since the epoch to a struct_time in UTC - #mktime: expresses the time in local time, not UTC. It returns a floating point number, for compatibility with time(). - epoch_start = calendar.timegm(time.strptime(plstart, '%Y-%m-%d-%H-%M-%S')) #Return the time as a floating point number expressed in seconds since the epoch, in UTC. @@ -186,7 +174,7 @@ class PypoPush(Thread): if loops % heartbeat_period == 0: logger.info("heartbeat") loops = 0 - try: self.push('scheduler') + try: self.push() except Exception, e: logger.error('Pypo Push Exception: %s', e) time.sleep(PUSH_INTERVAL) From 39fac2f88a2de078205f913cbab4aa1026054a70 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Tue, 28 Feb 2012 11:06:31 -0500 Subject: [PATCH 07/39] CC-3336: Refactor schedule API used by pypo -refactored pypo-push --- python_apps/pypo/pypopush.py | 129 +++++++++++++++-------------------- 1 file changed, 55 insertions(+), 74 deletions(-) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 2ecdbbe6e..a737d43be 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -87,84 +87,65 @@ class PypoPush(Thread): # Call API to update schedule states logger.debug("Doing callback to server to update 'played' status.") self.api_client.notify_scheduled_item_start_playing(pkey, schedule) - - + def push_to_liquidsoap(self, media_item): - if media_item["starts"] == self.last_end_time: - """ - this media item is attached to the end of the last - track, so let's push it now so that Liquidsoap can start playing - it immediately after (and prepare crossfades if need be). - """ - tn = telnetlib.Telnet(LS_HOST, LS_PORT) - tn.write(str('queue.push %s\n' % media_item["annotation"].encode('utf-8'))) - #TODO: vars.pypo_data - #TODO: vars.show_name - tn.write("exit\n") - - self.last_end_time = media_item["end"] - else: - """ - this media item does not start right after a current playing track. - We need to sleep, and then wake up when this track starts. - """ - - return False - - def push_liquidsoap_old(self, pkey, schedule, playlists): - logger = logging.getLogger('push') - try: - playlist = playlists[pkey] - plstart = schedule[pkey]['start'][0:19] - - #strptime returns struct_time in local time - epoch_start = calendar.timegm(time.strptime(plstart, '%Y-%m-%d-%H-%M-%S')) - - #Return the time as a floating point number expressed in seconds since the epoch, in UTC. - epoch_now = time.time() - - logger.debug("Epoch start: %s" % epoch_start) - logger.debug("Epoch now: %s" % epoch_now) - - sleep_time = epoch_start - epoch_now; - - if sleep_time < 0: - sleep_time = 0 - - logger.debug('sleeping for %s s' % (sleep_time)) - time.sleep(sleep_time) - - tn = telnetlib.Telnet(LS_HOST, LS_PORT) - - #skip the currently playing song if any. - logger.debug("source.skip\n") - tn.write("source.skip\n") - - # Get any extra information for liquidsoap (which will be sent back to us) - liquidsoap_data = self.api_client.get_liquidsoap_data(pkey, schedule) - - #Sending schedule table row id string. - logger.debug("vars.pypo_data %s\n"%(liquidsoap_data["schedule_id"])) - tn.write(("vars.pypo_data %s\n"%liquidsoap_data["schedule_id"]).encode('utf-8')) - - logger.debug('Preparing to push playlist %s' % pkey) - for item in playlist: - annotate = item['annotate'] - tn.write(str('queue.push %s\n' % annotate.encode('utf-8'))) - - show_name = item['show_name'] - tn.write(str('vars.show_name %s\n' % show_name.encode('utf-8'))) - - tn.write("exit\n") - logger.debug(tn.read_all()) - - status = 1 + if media_item["starts"] == self.last_end_time: + """ + this media item is attached to the end of the last + track, so let's push it now so that Liquidsoap can start playing + it immediately after (and prepare crossfades if need be). + """ + telnet_to_liquidsoap(media_item) + self.last_end_time = media_item["end"] + else: + """ + this media item does not start right after a current playing track. + We need to sleep, and then wake up when this track starts. + """ + sleep_until_start(media_item) + + telnet_to_liquidsoap(media_item) + self.last_end_time = media_item["end"] except Exception, e: - logger.error('%s', e) - status = 0 - return status + return False + + return True + def sleep_until_start(media_item): + mi_start = media_item['start'][0:19] + + #strptime returns struct_time in local time + epoch_start = calendar.timegm(time.strptime(mi_start, '%Y-%m-%d-%H-%M-%S')) + + #Return the time as a floating point number expressed in seconds since the epoch, in UTC. + epoch_now = time.time() + + logger.debug("Epoch start: %s" % epoch_start) + logger.debug("Epoch now: %s" % epoch_now) + + sleep_time = epoch_start - epoch_now + + if sleep_time < 0: + sleep_time = 0 + + logger.debug('sleeping for %s s' % (sleep_time)) + time.sleep(sleep_time) + + def telnet_to_liquidsoap(media_item): + tn = telnetlib.Telnet(LS_HOST, LS_PORT) + + #tn.write(("vars.pypo_data %s\n"%liquidsoap_data["schedule_id"]).encode('utf-8')) + + annotation = media_item['annotation'] + tn.write('queue.push %s\n' % annotation.encode('utf-8')) + + show_name = media_item['show_name'] + tn.write('vars.show_name %s\n' % show_name.encode('utf-8')) + + tn.write("exit\n") + logger.debug(tn.read_all()) + def run(self): loops = 0 heartbeat_period = math.floor(30/PUSH_INTERVAL) From 91ef7f766974558b5390d209452eab0fa6273fe2 Mon Sep 17 00:00:00 2001 From: james Date: Sat, 25 Feb 2012 09:44:33 -0500 Subject: [PATCH 08/39] CC-3346: Recorder: Merge recorder with pypo - function parse_show was renamed to process_recorder_schedule - using thread on self.process_schedule call --- python_apps/pypo/pypofetch.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 99c742d15..5ae5ffa92 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -15,6 +15,7 @@ from subprocess import Popen, PIPE from datetime import datetime from datetime import timedelta import filecmp +import thread # For RabbitMQ from kombu.connection import BrokerConnection @@ -82,7 +83,7 @@ class PypoFetch(Thread): if command == 'update_schedule': self.schedule_data = m['schedule'] - self.process_schedule(self.schedule_data, "scheduler", False) + thread.start_new_thread(self.process_schedule, (self.schedule_data, "scheduler", False)) elif command == 'update_stream_setting': logger.info("Updating stream setting...") self.regenerateLiquidsoapConf(m['setting']) @@ -98,7 +99,7 @@ class PypoFetch(Thread): elif command == 'update_recorder_schedule': temp = m if temp is not None: - self.parse_shows(temp) + self.process_recorder_schedule(temp) elif command == 'cancel_recording': self.recorder_queue.put('cancel_recording') except Exception, e: @@ -330,7 +331,7 @@ class PypoFetch(Thread): return datetime(date[0], date[1], date[2], time[0], time[1], time[2], 0, None) - def parse_shows(self, m): + def process_recorder_schedule(self, m): logger = logging.getLogger('fetch') logger.info("Parsing recording show schedules...") shows_to_record = {} @@ -517,14 +518,14 @@ class PypoFetch(Thread): status, self.schedule_data = self.api_client.get_schedule() if status == 1: logger.info("Bootstrap schedule received: %s", self.schedule_data) - self.process_schedule(self.schedule_data, "scheduler", True) + thread.start_new_thread(self.process_schedule, (self.schedule_data, "scheduler", True)) # Bootstrap: since we are just starting up, we need to grab the # most recent schedule. After that we can just wait for updates. try: temp = self.api_client.get_shows_to_record() if temp is not None: - self.parse_shows(temp) + self.process_recorder_schedule(temp) logger.info("Bootstrap recorder schedule received: %s", temp) except Exception, e: logger.error(e) @@ -556,14 +557,14 @@ class PypoFetch(Thread): status, self.schedule_data = self.api_client.get_schedule() if status == 1: - self.process_schedule(self.schedule_data, "scheduler", False) + thread.start_new_thread(self.process_schedule, (self.schedule_data, "scheduler", False)) """ Fetch recorder schedule """ try: temp = self.api_client.get_shows_to_record() if temp is not None: - self.parse_shows(temp) + self.process_recorder_schedule(temp) logger.info("updated recorder schedule received: %s", temp) except Exception, e: logger.error(e) From 322d1bfa99448fc3132a6ac5d95d74d56553dbef Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 22 Feb 2012 17:29:49 -0500 Subject: [PATCH 09/39] CC-3336: Refactor schedule API used by pypo -initial commit --- .../application/controllers/ApiController.php | 2 + airtime_mvc/application/models/Schedule.php | 184 ++++++++++-------- 2 files changed, 104 insertions(+), 82 deletions(-) diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index 2ea6beb89..b82366e08 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -276,12 +276,14 @@ class ApiController extends Zend_Controller_Action $api_key = $this->_getParam('api_key'); + /* if(!in_array($api_key, $CC_CONFIG["apiKey"])) { header('HTTP/1.0 401 Unauthorized'); print 'You are not allowed to access this resource. '; exit; } + * */ PEAR::setErrorHandling(PEAR_ERROR_RETURN); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 367278bc7..1cf0dfaa6 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -46,87 +46,6 @@ class Application_Model_Schedule { } - /** - * Returns array indexed by: - * "playlistId"/"playlist_id" (aliases to the same thing) - * "start"/"starts" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn - * "end"/"ends" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn - * "group_id"/"id" (aliases to the same thing) - * "clip_length" (for audio clips this is the length of the audio clip, - * for playlists this is the length of the entire playlist) - * "name" (playlist only) - * "creator" (playlist only) - * "file_id" (audioclip only) - * "count" (number of items in the playlist, always 1 for audioclips. - * Note that playlists with one item will also have count = 1. - * - * @param string $p_fromDateTime - * In the format YYYY-MM-DD HH:MM:SS.nnnnnn - * @param string $p_toDateTime - * In the format YYYY-MM-DD HH:MM:SS.nnnnnn - * @param boolean $p_playlistsOnly - * Retrieve playlists as a single item. - * @return array - * Returns empty array if nothing found - */ - - public static function GetItems($p_currentDateTime, $p_toDateTime, $p_playlistsOnly = true) - { - global $CC_CONFIG, $CC_DBC; - $rows = array(); - if (!$p_playlistsOnly) { - $sql = "SELECT * FROM ".$CC_CONFIG["scheduleTable"] - ." WHERE (starts >= TIMESTAMP '$p_currentDateTime') " - ." AND (ends <= TIMESTAMP '$p_toDateTime')"; - $rows = $CC_DBC->GetAll($sql); - foreach ($rows as &$row) { - $row["count"] = "1"; - $row["playlistId"] = $row["playlist_id"]; - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - $row["id"] = $row["group_id"]; - } - } else { - $sql = "SELECT MIN(pt.creator) AS creator," - ." st.group_id," - ." SUM(st.clip_length) AS clip_length," - ." MIN(st.file_id) AS file_id," - ." COUNT(*) as count," - ." MIN(st.playlist_id) AS playlist_id," - ." MIN(st.starts) AS starts," - ." MAX(st.ends) AS ends," - ." MIN(sh.name) AS show_name," - ." MIN(si.starts) AS show_start," - ." MAX(si.ends) AS show_end" - ." FROM $CC_CONFIG[scheduleTable] as st" - ." LEFT JOIN $CC_CONFIG[playListTable] as pt" - ." ON st.playlist_id = pt.id" - ." LEFT JOIN $CC_CONFIG[showInstances] as si" - ." ON st.instance_id = si.id" - ." LEFT JOIN $CC_CONFIG[showTable] as sh" - ." ON si.show_id = sh.id" - //The next line ensures we only get songs that haven't ended yet - ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" - ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" - //next line makes sure that we aren't returning items that - //are past the show's scheduled timeslot. - ." AND (st.starts < si.ends)" - ." GROUP BY st.group_id" - ." ORDER BY starts"; - - $rows = $CC_DBC->GetAll($sql); - if (!PEAR::isError($rows)) { - foreach ($rows as &$row) { - $row["playlistId"] = $row["playlist_id"]; - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - $row["id"] = $row["group_id"]; - } - } - } - return $rows; - } - /** * Returns data related to the scheduled items. * @@ -475,7 +394,108 @@ class Application_Model_Schedule { } return $diff; } + + /** + * Returns array indexed by: + * "playlistId"/"playlist_id" (aliases to the same thing) + * "start"/"starts" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn + * "end"/"ends" (aliases to the same thing) as YYYY-MM-DD HH:MM:SS.nnnnnn + * "group_id"/"id" (aliases to the same thing) + * "clip_length" (for audio clips this is the length of the audio clip, + * for playlists this is the length of the entire playlist) + * "name" (playlist only) + * "creator" (playlist only) + * "file_id" (audioclip only) + * "count" (number of items in the playlist, always 1 for audioclips. + * Note that playlists with one item will also have count = 1. + * + * @param string $p_fromDateTime + * In the format YYYY-MM-DD HH:MM:SS.nnnnnn + * @param string $p_toDateTime + * In the format YYYY-MM-DD HH:MM:SS.nnnnnn + * @param boolean $p_playlistsOnly + * Retrieve playlists as a single item. + * @return array + * Returns null if nothing found + */ + public static function GetItems($p_currentDateTime, $p_toDateTime) + { + global $CC_CONFIG, $CC_DBC; + $rows = array(); + + $sql = "SELECT st.file_id AS file_id," + ." st.starts AS starts," + ." st.ends AS ends," + ." si.starts as show_start," + ." si.ends as show_end" + ." FROM $CC_CONFIG[scheduleTable] as st" + ." LEFT JOIN $CC_CONFIG[showInstances] as si" + ." ON st.instance_id = si.id" + ." ORDER BY starts"; + + + /* + $sql = "SELECT pt.creator as creator" + ." st.file_id AS file_id" + ." st.starts AS starts" + ." st.ends AS ends" + ." st.name as show_name" + ." si.starts as show_start" + ." si.ends as show_end" + ." FROM $CC_CONFIG[scheduleTable] as st" + ." LEFT JOIN $CC_CONFIG[showInstances] as si" + ." ON st.instance_id = si.id" + ." LEFT JOIN $CC_CONFIG[showTable] as sh" + ." ON si.show_id = sh.id" + //The next line ensures we only get songs that haven't ended yet + ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" + ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" + //next line makes sure that we aren't returning items that + //are past the show's scheduled timeslot. + ." AND (st.starts < si.ends)" + ." ORDER BY starts"; + * */ + + $rows = $CC_DBC->GetAll($sql); + if (!PEAR::isError($rows)) { + foreach ($rows as &$row) { + $row["start"] = $row["starts"]; + $row["end"] = $row["ends"]; + } + } + + return $rows; + } + + + public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null){ + + global $CC_CONFIG, $CC_DBC; + + /* 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()); + $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()); + $t2->add(new DateInterval("PT24H")); + $range_end = $t2->format("Y-m-d H:i:s"); + } else { + $range_end = Application_Model_Schedule::PypoTimeToAirtimeTime($p_toDateTime); + } + + // Scheduler wants everything in a playlist + $data = Application_Model_Schedule::GetItems($range_start, $range_end); + + Logging::log(print_r($data, true)); + + return $data; + } /** * Export the schedule in json formatted for pypo (the liquidsoap scheduler) @@ -485,7 +505,7 @@ class Application_Model_Schedule { * @param string $p_toDateTime * In the format "YYYY-MM-DD-HH-mm-SS" */ - public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null) + public static function GetScheduledPlaylistsOld($p_fromDateTime = null, $p_toDateTime = null) { global $CC_CONFIG, $CC_DBC; From 5aabe89069a1270a6a10a9bf46cc815011cacb9b Mon Sep 17 00:00:00 2001 From: James Date: Tue, 28 Feb 2012 14:44:39 -0500 Subject: [PATCH 10/39] CC-3346: Recorder: Merge recorder with pypo - separated rabitMQ listener part out from pypoFetch and created pypomessagehandler.py Conflicts: python_apps/pypo/pypofetch.py --- python_apps/pypo/logging.cfg | 16 ++- python_apps/pypo/pypo-cli.py | 21 ++-- python_apps/pypo/pypofetch.py | 137 ++++++++----------------- python_apps/pypo/pypomessagehandler.py | 120 ++++++++++++++++++++++ python_apps/pypo/recorder.py | 56 ++++++++-- 5 files changed, 238 insertions(+), 112 deletions(-) create mode 100644 python_apps/pypo/pypomessagehandler.py diff --git a/python_apps/pypo/logging.cfg b/python_apps/pypo/logging.cfg index acff7007d..6dae7d9c5 100644 --- a/python_apps/pypo/logging.cfg +++ b/python_apps/pypo/logging.cfg @@ -1,8 +1,8 @@ [loggers] -keys=root,fetch,push,recorder +keys=root,fetch,push,recorder,message_h [handlers] -keys=pypo,recorder +keys=pypo,recorder,message_h [formatters] keys=simpleFormatter @@ -29,6 +29,12 @@ handlers=recorder qualname=recorder propagate=0 +[logger_message_h] +level=DEBUG +handlers=message_h +qualname=message_h +propagate=0 + [handler_pypo] class=logging.handlers.RotatingFileHandler level=DEBUG @@ -41,6 +47,12 @@ level=DEBUG formatter=simpleFormatter args=("/var/log/airtime/pypo/show-recorder.log", 'a', 1000000, 5,) +[handler_message_h] +class=logging.handlers.RotatingFileHandler +level=DEBUG +formatter=simpleFormatter +args=("/var/log/airtime/pypo/message-handler.log", 'a', 1000000, 5,) + [formatter_simpleFormatter] format=%(asctime)s %(levelname)s - [%(filename)s : %(funcName)s() : line %(lineno)d] - %(message)s datefmt= diff --git a/python_apps/pypo/pypo-cli.py b/python_apps/pypo/pypo-cli.py index d82d0b782..1fcf6e59c 100644 --- a/python_apps/pypo/pypo-cli.py +++ b/python_apps/pypo/pypo-cli.py @@ -16,6 +16,7 @@ from Queue import Queue from pypopush import PypoPush from pypofetch import PypoFetch from recorder import Recorder +from pypomessagehandler import PypoMessageHandler from configobj import ConfigObj @@ -127,11 +128,19 @@ if __name__ == '__main__': api_client = api_client.api_client_factory(config) api_client.register_component("pypo") - q = Queue() - + pypoFetch_q = Queue() recorder_q = Queue() - - pp = PypoPush(q) + pypoPush_q = Queue() + + pmh = PypoMessageHandler(pypoFetch_q, recorder_q) + pmh.daemon = True + pmh.start() + + pf = PypoFetch(pypoFetch_q, pypoPush_q) + pf.daemon = True + pf.start() + + pp = PypoPush(pypoPush_q) pp.daemon = True pp.start() @@ -139,10 +148,6 @@ if __name__ == '__main__': recorder.daemon = True recorder.start() - pf = PypoFetch(q, recorder_q) - pf.daemon = True - pf.start() - #pp.join() pf.join() logger.info("pypo fetch exit") diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 5ae5ffa92..860ed781e 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -15,13 +15,6 @@ from subprocess import Popen, PIPE from datetime import datetime from datetime import timedelta import filecmp -import thread - -# For RabbitMQ -from kombu.connection import BrokerConnection -from kombu.messaging import Exchange, Queue, Consumer, Producer -from kombu.exceptions import MessageStateError -from kombu.simple import SimpleQueue from api_clients import api_client @@ -43,30 +36,15 @@ except Exception, e: sys.exit() class PypoFetch(Thread): - def __init__(self, q, recorder_q): + def __init__(self, pypoFetch_q, pypoPush_q): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) self.set_export_source('scheduler') - self.queue = q - self.recorder_queue = recorder_q + self.fetch_queue = pypoFetch_q + self.push_queue = pypoPush_q self.schedule_data = [] logger = logging.getLogger('fetch') logger.info("PypoFetch: init complete") - - def init_rabbit_mq(self): - logger = logging.getLogger('fetch') - logger.info("Initializing RabbitMQ stuff") - try: - schedule_exchange = Exchange("airtime-pypo", "direct", durable=True, auto_delete=True) - schedule_queue = Queue("pypo-fetch", exchange=schedule_exchange, key="foo") - connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], config["rabbitmq_vhost"]) - channel = connection.channel() - self.simple_queue = SimpleQueue(channel, schedule_queue) - except Exception, e: - logger.error(e) - return False - - return True """ Handle a message from RabbitMQ, put it into our yucky global var. @@ -83,7 +61,7 @@ class PypoFetch(Thread): if command == 'update_schedule': self.schedule_data = m['schedule'] - thread.start_new_thread(self.process_schedule, (self.schedule_data, "scheduler", False)) + self.process_schedule(self.schedule_data, "scheduler", False) elif command == 'update_stream_setting': logger.info("Updating stream setting...") self.regenerateLiquidsoapConf(m['setting']) @@ -96,13 +74,11 @@ class PypoFetch(Thread): elif command == 'cancel_current_show': logger.info("Cancel current show command received...") self.stop_current_show() - elif command == 'update_recorder_schedule': - temp = m - if temp is not None: - self.process_recorder_schedule(temp) - elif command == 'cancel_recording': - self.recorder_queue.put('cancel_recording') except Exception, e: + import traceback + top = traceback.format_exc() + logger.error('Exception: %s', e) + logger.error("traceback: %s", top) logger.error("Exception in handling RabbitMQ message: %s", e) def stop_current_show(self): @@ -315,36 +291,11 @@ class PypoFetch(Thread): scheduled_data = dict() scheduled_data['liquidsoap_playlists'] = liquidsoap_playlists scheduled_data['schedule'] = playlists - self.queue.put(scheduled_data) + self.push_queue.put(scheduled_data) # cleanup try: self.cleanup(self.export_source) except Exception, e: logger.error("%s", e) - - def getDateTimeObj(self,time): - timeinfo = time.split(" ") - date = timeinfo[0].split("-") - time = timeinfo[1].split(":") - - date = map(int, date) - time = map(int, time) - - return datetime(date[0], date[1], date[2], time[0], time[1], time[2], 0, None) - - def process_recorder_schedule(self, m): - logger = logging.getLogger('fetch') - logger.info("Parsing recording show schedules...") - shows_to_record = {} - shows = m['shows'] - for show in shows: - show_starts = self.getDateTimeObj(show[u'starts']) - show_end = self.getDateTimeObj(show[u'ends']) - time_delta = show_end - show_starts - - shows_to_record[show[u'starts']] = [time_delta, show[u'instance_id'], show[u'name'], m['server_timezone']] - self.recorder_queue.put(shows_to_record) - logger.info(shows_to_record) - """ In this function every audio file is cut as necessary (cue_in/cue_out != 0) @@ -518,36 +469,46 @@ class PypoFetch(Thread): status, self.schedule_data = self.api_client.get_schedule() if status == 1: logger.info("Bootstrap schedule received: %s", self.schedule_data) - thread.start_new_thread(self.process_schedule, (self.schedule_data, "scheduler", True)) - - # Bootstrap: since we are just starting up, we need to grab the - # most recent schedule. After that we can just wait for updates. - try: - temp = self.api_client.get_shows_to_record() - if temp is not None: - self.process_recorder_schedule(temp) - logger.info("Bootstrap recorder schedule received: %s", temp) - except Exception, e: - logger.error(e) - - logger.info("Bootstrap complete: got initial copy of the schedule") - - - while not self.init_rabbit_mq(): - logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds") - time.sleep(5) + self.process_schedule(self.schedule_data, "scheduler", True) loops = 1 while True: logger.info("Loop #%s", loops) try: try: - message = self.simple_queue.get(block=True) - self.handle_message(message.payload) - # ACK the message to take it off the queue - message.ack() - except MessageStateError, m: - logger.error("Message ACK error: %s", m) + """ + our simple_queue.get() requires a timeout, in which case we + fetch the Airtime schedule manually. It is important to fetch + the schedule periodically because if we didn't, we would only + get schedule updates via RabbitMq if the user was constantly + using the Airtime interface. + + If the user is not using the interface, RabbitMq messages are not + sent, and we will have very stale (or non-existent!) data about the + schedule. + + Currently we are checking every 3600 seconds (1 hour) + """ + message = self.fetch_queue.get(block=True, timeout=3600) + self.handle_message(message) + except Empty, e: + """ + Queue timeout. Fetching data manually + """ + raise + except Exception, e: + """ + sleep 5 seconds so that we don't spin inside this + while loop and eat all the CPU + """ + time.sleep(5) + + """ + There is a problem with the RabbitMq messenger service. Let's + log the error and get the schedule via HTTP polling + """ + logger.error("Exception, %s", e) + raise except Exception, e: """ There is a problem with the RabbitMq messenger service. Let's @@ -557,17 +518,7 @@ class PypoFetch(Thread): status, self.schedule_data = self.api_client.get_schedule() if status == 1: - thread.start_new_thread(self.process_schedule, (self.schedule_data, "scheduler", False)) - """ - Fetch recorder schedule - """ - try: - temp = self.api_client.get_shows_to_record() - if temp is not None: - self.process_recorder_schedule(temp) - logger.info("updated recorder schedule received: %s", temp) - except Exception, e: - logger.error(e) + self.process_schedule(self.schedule_data, "scheduler", False) loops += 1 diff --git a/python_apps/pypo/pypomessagehandler.py b/python_apps/pypo/pypomessagehandler.py new file mode 100644 index 000000000..75b0407ea --- /dev/null +++ b/python_apps/pypo/pypomessagehandler.py @@ -0,0 +1,120 @@ +import logging +import logging.config +import sys +from configobj import ConfigObj +from threading import Thread +import time +# For RabbitMQ +from kombu.connection import BrokerConnection +from kombu.messaging import Exchange, Queue, Consumer, Producer +from kombu.exceptions import MessageStateError +from kombu.simple import SimpleQueue +import json + +# configure logging +logging.config.fileConfig("logging.cfg") + +# loading config file +try: + config = ConfigObj('/etc/airtime/pypo.cfg') + LS_HOST = config['ls_host'] + LS_PORT = config['ls_port'] + POLL_INTERVAL = int(config['poll_interval']) + +except Exception, e: + logger = logging.getLogger('message_h') + logger.error('Error loading config file: %s', e) + sys.exit() + +class PypoMessageHandler(Thread): + def __init__(self, pq, rq): + Thread.__init__(self) + self.logger = logging.getLogger('message_h') + self.pypo_queue = pq + self.recorder_queue = rq + + def init_rabbit_mq(self): + self.logger.info("Initializing RabbitMQ stuff") + try: + schedule_exchange = Exchange("airtime-pypo", "direct", durable=True, auto_delete=True) + schedule_queue = Queue("pypo-fetch", exchange=schedule_exchange, key="foo") + connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], config["rabbitmq_vhost"]) + channel = connection.channel() + self.simple_queue = SimpleQueue(channel, schedule_queue) + except Exception, e: + self.logger.error(e) + return False + + return True + + """ + Handle a message from RabbitMQ, put it into our yucky global var. + Hopefully there is a better way to do this. + """ + def handle_message(self, message): + try: + self.logger.info("Received event from RabbitMQ: %s" % message) + + m = json.loads(message) + command = m['event_type'] + self.logger.info("Handling command: " + command) + + if command == 'update_schedule': + self.logger.info("Updating schdule...") + self.pypo_queue.put(message) + elif command == 'update_stream_setting': + self.logger.info("Updating stream setting...") + self.pypo_queue.put(message) + elif command == 'update_stream_format': + self.logger.info("Updating stream format...") + self.pypo_queue.put(message) + elif command == 'update_station_name': + self.logger.info("Updating station name...") + self.pypo_queue.put(message) + elif command == 'cancel_current_show': + self.logger.info("Cancel current show command received...") + self.pypo_queue.put(message) + elif command == 'update_recorder_schedule': + self.recorder_queue.put(message) + elif command == 'cancel_recording': + self.recorder_queue.put(message) + except Exception, e: + self.logger.error("Exception in handling RabbitMQ message: %s", e) + + def main(self): + while not self.init_rabbit_mq(): + self.logger.error("Error connecting to RabbitMQ Server. Trying again in few seconds") + time.sleep(5) + + loops = 1 + while True: + self.logger.info("Loop #%s", loops) + try: + message = self.simple_queue.get(block=True) + self.handle_message(message.payload) + # ACK the message to take it off the queue + message.ack() + except Exception, e: + """ + sleep 5 seconds so that we don't spin inside this + while loop and eat all the CPU + """ + time.sleep(5) + + """ + There is a problem with the RabbitMq messenger service. Let's + log the error and get the schedule via HTTP polling + """ + self.logger.error("Exception, %s", e) + + loops += 1 + + """ + Main loop of the thread: + Wait for schedule updates from RabbitMQ, but in case there arent any, + poll the server to get the upcoming schedule. + """ + def run(self): + while True: + self.main() + diff --git a/python_apps/pypo/recorder.py b/python_apps/pypo/recorder.py index 6347c4f76..a4052c485 100644 --- a/python_apps/pypo/recorder.py +++ b/python_apps/pypo/recorder.py @@ -176,19 +176,35 @@ class Recorder(Thread): self.server_timezone = '' self.queue = q self.logger.info("RecorderFetch: init complete") + self.loops = 0 def handle_message(self): if not self.queue.empty(): - msg = self.queue.get() - self.logger.info("Receivied msg from Pypo Fetch: %s", msg) - if msg == 'cancel_recording': + message = self.queue.get() + msg = json.loads(message) + command = msg["event_type"] + self.logger.info("Received msg from Pypo Fetch: %s", msg) + if command == 'cancel_recording': if self.sr is not None and self.sr.is_recording(): self.sr.cancel_recording() else: - self.shows_to_record = msg + self.process_recorder_schedule(msg) + self.loops = 0 if self.shows_to_record: self.start_record() + + def process_recorder_schedule(self, m): + self.logger.info("Parsing recording show schedules...") + temp_shows_to_record = {} + shows = m['shows'] + for show in shows: + show_starts = getDateTimeObj(show[u'starts']) + show_end = getDateTimeObj(show[u'ends']) + time_delta = show_end - show_starts + + temp_shows_to_record[show[u'starts']] = [time_delta, show[u'instance_id'], show[u'name'], m['server_timezone']] + self.shows_to_record = temp_shows_to_record def get_time_till_next_show(self): if len(self.shows_to_record) != 0: @@ -247,21 +263,43 @@ class Recorder(Thread): def run(self): try: self.logger.info("Started...") - + # Bootstrap: since we are just starting up, we need to grab the + # most recent schedule. After that we can just wait for updates. + try: + temp = self.api_client.get_shows_to_record() + if temp is not None: + self.process_recorder_schedule(temp) + self.logger.info("Bootstrap recorder schedule received: %s", temp) + except Exception, e: + self.logger.error(e) + + self.logger.info("Bootstrap complete: got initial copy of the schedule") + recording = False - loops = 0 + self.loops = 0 heartbeat_period = math.floor(30/PUSH_INTERVAL) while True: - if loops % heartbeat_period == 0: + if self.loops % heartbeat_period == 0: self.logger.info("heartbeat") - loops = 0 + if self.loops * PUSH_INTERVAL > 3600: + self.loops = 0 + """ + Fetch recorder schedule + """ + try: + temp = self.api_client.get_shows_to_record() + if temp is not None: + self.process_recorder_schedule(temp) + self.logger.info("updated recorder schedule received: %s", temp) + except Exception, e: + self.logger.error(e) try: self.handle_message() except Exception, e: self.logger.error('Pypo Recorder Exception: %s', e) time.sleep(PUSH_INTERVAL) - loops += 1 + self.loops += 1 except Exception,e : import traceback top = traceback.format_exc() From d4cdac505a391978606f7b7ef7b5a67cbf90465a Mon Sep 17 00:00:00 2001 From: James Date: Mon, 27 Feb 2012 18:09:07 -0500 Subject: [PATCH 11/39] CC-3346: Recorder: Merge recorder with pypo - fixed log message --- python_apps/pypo/pypofetch.py | 4 ++-- python_apps/pypo/recorder.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 860ed781e..4328260a5 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -53,7 +53,7 @@ class PypoFetch(Thread): def handle_message(self, message): try: logger = logging.getLogger('fetch') - logger.info("Received event from RabbitMQ: %s" % message) + logger.info("Received event from Pypo Message Handler: %s" % message) m = json.loads(message) command = m['event_type'] @@ -79,7 +79,7 @@ class PypoFetch(Thread): top = traceback.format_exc() logger.error('Exception: %s', e) logger.error("traceback: %s", top) - logger.error("Exception in handling RabbitMQ message: %s", e) + logger.error("Exception in handling Message Handler message: %s", e) def stop_current_show(self): logger = logging.getLogger('fetch') diff --git a/python_apps/pypo/recorder.py b/python_apps/pypo/recorder.py index a4052c485..cd57617d9 100644 --- a/python_apps/pypo/recorder.py +++ b/python_apps/pypo/recorder.py @@ -183,7 +183,7 @@ class Recorder(Thread): message = self.queue.get() msg = json.loads(message) command = msg["event_type"] - self.logger.info("Received msg from Pypo Fetch: %s", msg) + self.logger.info("Received msg from Pypo Message Handler: %s", msg) if command == 'cancel_recording': if self.sr is not None and self.sr.is_recording(): self.sr.cancel_recording() From d8f7cce56ec254091717854614de272fd5aabf33 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 28 Feb 2012 10:00:43 -0500 Subject: [PATCH 12/39] CC-3346: Recorder will not record shows if calendar is not touched 2 hours before the recorder starts - apiclient should log to correct log file. --- python_apps/pypo/recorder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_apps/pypo/recorder.py b/python_apps/pypo/recorder.py index cd57617d9..89a95b001 100644 --- a/python_apps/pypo/recorder.py +++ b/python_apps/pypo/recorder.py @@ -169,7 +169,7 @@ class Recorder(Thread): def __init__(self, q): Thread.__init__(self) self.logger = logging.getLogger('recorder') - self.api_client = api_client.api_client_factory(config) + self.api_client = api_client.api_client_factory(config, self.logger) self.api_client.register_component("show-recorder") self.sr = None self.shows_to_record = {} From 99c24ed0382bf6d6a15b86e64b9b9ea82e6ddf48 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 22 Feb 2012 20:41:24 -0500 Subject: [PATCH 13/39] CC-3336: Refactor schedule API used by pypo -removed export_source -rewrote GetScheduledPlaylists() --- airtime_mvc/application/models/Schedule.php | 47 +++++++++++++++++++-- python_apps/pypo/pypo-cli.py | 9 +--- python_apps/pypo/pypofetch.py | 36 +++++++++------- python_apps/pypo/pypopush.py | 8 +--- 4 files changed, 65 insertions(+), 35 deletions(-) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 1cf0dfaa6..6e6598a86 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -468,6 +468,19 @@ class Application_Model_Schedule { return $rows; } +/* + "2012-02-23-01-00-00":{ + "row_id":"1", + "id":"caf951f6d8f087c3a90291a9622073f9", + "uri":"http:\/\/localhost:80\/api\/get-media\/file\/caf951f6d8f087c3a90291a9622073f9.mp3", + "fade_in":0, + "fade_out":0, + "cue_in":0, + "cue_out":199.798, + "start":"2012-02-23-01-00-00", + "end":"2012-02-23-01-03-19" + } + * */ public static function GetScheduledPlaylists($p_fromDateTime = null, $p_toDateTime = null){ @@ -490,10 +503,38 @@ class Application_Model_Schedule { } // Scheduler wants everything in a playlist - $data = Application_Model_Schedule::GetItems($range_start, $range_end); + $items = Application_Model_Schedule::GetItems($range_start, $range_end); - Logging::log(print_r($data, true)); + $data = array(); + $utcTimeZone = new DateTimeZone("UTC"); + foreach ($items as $item){ + + $storedFile = Application_Model_StoredFile::Recall($item["file_id"]); + $uri = $storedFile->getFileUrlUsingConfigAddress(); + + $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone); + $trackEndDateTime = new DateTime($item["ends"], $utcTimeZone); + + if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()){ + $diff = $trackEndDateTime->getTimestamp() - $showEndDateTime->getTimestamp(); + //assuming ends takes cue_out into assumption + $item["cue_out"] = $item["cue_out"] - $diff; + } + + $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); + $data[$starts] = array( + 'id' => $storedFile->getGunid(), + 'uri' => $uri, + 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), + 'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]), + 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), + 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), + 'start' => $starts, + 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) + ); + } + return $data; } @@ -566,7 +607,6 @@ class Application_Model_Schedule { $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); $medias[$starts] = array( - 'row_id' => $item["id"], 'id' => $storedFile->getGunid(), 'uri' => $uri, 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), @@ -574,7 +614,6 @@ class Application_Model_Schedule { 'fade_cross' => 0, 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), - 'export_source' => 'scheduler', 'start' => $starts, 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) ); diff --git a/python_apps/pypo/pypo-cli.py b/python_apps/pypo/pypo-cli.py index 1fcf6e59c..8bfd7d026 100644 --- a/python_apps/pypo/pypo-cli.py +++ b/python_apps/pypo/pypo-cli.py @@ -56,23 +56,16 @@ except Exception, e: class Global: def __init__(self): self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') def selfcheck(self): self.api_client = api_client.api_client_factory(config) return self.api_client.is_server_compatible() - - def set_export_source(self, export_source): - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - self.schedule_file = self.cache_dir + 'schedule.pickle' - self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle" def test_api(self): self.api_client.test() """ - def check_schedule(self, export_source): + def check_schedule(self): logger = logging.getLogger() try: diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 4328260a5..d0d0e0e32 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -39,9 +39,22 @@ class PypoFetch(Thread): def __init__(self, pypoFetch_q, pypoPush_q): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') + self.fetch_queue = pypoFetch_q self.push_queue = pypoPush_q + + self.cache_dir = os.path.join(config["cache_dir"], "scheduler") + logger.debug("Cache dir %s", self.cache_dir) + try: + if not os.path.exists(dir): + logger.debug("Cache dir does not exist. Creating...") + os.makedirs(dir) + except Exception, e: + logger.error(e) + + + + self.schedule_data = [] logger = logging.getLogger('fetch') logger.info("PypoFetch: init complete") @@ -229,15 +242,6 @@ class PypoFetch(Thread): if(status == "true"): self.api_client.notify_liquidsoap_status("OK", stream_id, str(fake_time)) - - - def set_export_source(self, export_source): - logger = logging.getLogger('fetch') - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - logger.info("Creating cache directory at %s", self.cache_dir) - - def update_liquidsoap_stream_format(self, stream_format): # Push stream metadata to liquidsoap # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!! @@ -278,7 +282,7 @@ class PypoFetch(Thread): to the cache dir (Folder-structure: cache/YYYY-MM-DD-hh-mm-ss) - runs the cleanup routine, to get rid of unused cached files """ - def process_schedule(self, schedule_data, export_source, bootstrapping): + def process_schedule(self, schedule_data, bootstrapping): logger = logging.getLogger('fetch') playlists = schedule_data["playlists"] @@ -294,7 +298,7 @@ class PypoFetch(Thread): self.push_queue.put(scheduled_data) # cleanup - try: self.cleanup(self.export_source) + try: self.cleanup() except Exception, e: logger.error("%s", e) """ @@ -375,7 +379,7 @@ class PypoFetch(Thread): fileExt = os.path.splitext(media['uri'])[1] try: - dst = "%s%s/%s%s" % (self.cache_dir, pkey, media['id'], fileExt) + dst = os.path.join(self.cache_dir, pkey, media['id']+fileExt) # download media file self.handle_remote_file(media, dst) @@ -389,8 +393,8 @@ class PypoFetch(Thread): if fsize > 0: pl_entry = \ - 'annotate:export_source="%s",media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ - % (media['export_source'], media['id'], 0, \ + 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ + % (media['id'], 0, \ float(media['fade_in']) / 1000, \ float(media['fade_out']) / 1000, \ float(media['cue_in']), \ @@ -435,7 +439,7 @@ class PypoFetch(Thread): Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR" and deletes them. """ - def cleanup(self, export_source): + def cleanup(self): logger = logging.getLogger('fetch') offset = 3600 * int(config["cache_for"]) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 24f48c7cb..1d7c132c9 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -34,7 +34,6 @@ class PypoPush(Thread): def __init__(self, q): Thread.__init__(self) self.api_client = api_client.api_client_factory(config) - self.set_export_source('scheduler') self.queue = q self.schedule = dict() @@ -42,11 +41,6 @@ class PypoPush(Thread): self.liquidsoap_state_play = True self.push_ahead = 10 - - def set_export_source(self, export_source): - self.export_source = export_source - self.cache_dir = config["cache_dir"] + self.export_source + '/' - self.schedule_tracker_file = self.cache_dir + "schedule_tracker.pickle" """ The Push Loop - the push loop periodically checks if there is a playlist @@ -54,7 +48,7 @@ class PypoPush(Thread): If yes, the current liquidsoap playlist gets replaced with the corresponding one, then liquidsoap is asked (via telnet) to reload and immediately play it. """ - def push(self, export_source): + def push(self): logger = logging.getLogger('push') timenow = time.time() From a53d856e8d8a876c7ba9d16e71ecb8f7173b52f6 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Thu, 23 Feb 2012 11:21:16 -0500 Subject: [PATCH 14/39] CC-3336: Refactor schedule API used by pypo -add comment --- airtime_mvc/application/models/Schedule.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 6e6598a86..2cd573ef6 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -516,6 +516,9 @@ class Application_Model_Schedule { $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone); $trackEndDateTime = new DateTime($item["ends"], $utcTimeZone); + /* Note: cue_out and end are always the same. */ + /* TODO: Not all tracks will have "show_end" */ + if ($trackEndDateTime->getTimestamp() > $showEndDateTime->getTimestamp()){ $diff = $trackEndDateTime->getTimestamp() - $showEndDateTime->getTimestamp(); //assuming ends takes cue_out into assumption From 4386690b5401946a7458d43315e53a910234b179 Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Tue, 28 Feb 2012 20:58:06 +0100 Subject: [PATCH 15/39] CC-3174 : showbuilder prevents scheduling outside a show. --- airtime_mvc/application/models/ShowBuilder.php | 2 +- airtime_mvc/public/js/airtime/showbuilder/builder.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/airtime_mvc/application/models/ShowBuilder.php b/airtime_mvc/application/models/ShowBuilder.php index e3804e17f..46559c7f0 100644 --- a/airtime_mvc/application/models/ShowBuilder.php +++ b/airtime_mvc/application/models/ShowBuilder.php @@ -193,7 +193,7 @@ class Application_Model_ShowBuilder { private function makeFooterRow($p_item) { $row = $this->defaultRowArray; - $this->isAllowed($p_item, $row); + //$this->isAllowed($p_item, $row); $row["footer"] = true; $showEndDT = new DateTime($p_item["si_ends"], new DateTimeZone("UTC")); diff --git a/airtime_mvc/public/js/airtime/showbuilder/builder.js b/airtime_mvc/public/js/airtime/showbuilder/builder.js index 084fda9ff..577b6a418 100644 --- a/airtime_mvc/public/js/airtime/showbuilder/builder.js +++ b/airtime_mvc/public/js/airtime/showbuilder/builder.js @@ -287,6 +287,9 @@ $(document).ready(function() { if (aData.allowed !== true) { $(nRow).addClass("sb-not-allowed"); } + else { + $(nRow).addClass("sb-allowed"); + } //status used to colour tracks. if (aData.status === 1) { @@ -538,8 +541,8 @@ $(document).ready(function() { var prev = ui.item.prev(); //can't add items outside of shows. - if (prev.hasClass("sb-footer")) { - alert("Cannot add an item outside a show."); + if (!prev.hasClass("sb-allowed")) { + alert("Cannot schedule outside a show."); ui.item.remove(); return; } From 11f31effcaede565e76ea0635e440839cdebf4f6 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Mon, 27 Feb 2012 13:52:35 -0500 Subject: [PATCH 16/39] CC-3336: Refactor schedule API used by pypo --- airtime_mvc/application/models/Schedule.php | 53 +-- python_apps/api_clients/api_client.py | 6 +- python_apps/pypo/pypofetch.py | 368 +++++++++----------- python_apps/pypo/pypopush.py | 94 +++-- 4 files changed, 222 insertions(+), 299 deletions(-) diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index 2cd573ef6..979bbdbcc 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -425,46 +425,22 @@ class Application_Model_Schedule { $rows = array(); $sql = "SELECT st.file_id AS file_id," - ." st.starts AS starts," - ." st.ends AS ends," + ." st.starts AS start," + ." st.ends AS end," ." si.starts as show_start," ." si.ends as show_end" ." FROM $CC_CONFIG[scheduleTable] as st" ." LEFT JOIN $CC_CONFIG[showInstances] as si" ." ON st.instance_id = si.id" - ." ORDER BY starts"; - - - /* - $sql = "SELECT pt.creator as creator" - ." st.file_id AS file_id" - ." st.starts AS starts" - ." st.ends AS ends" - ." st.name as show_name" - ." si.starts as show_start" - ." si.ends as show_end" - ." FROM $CC_CONFIG[scheduleTable] as st" - ." LEFT JOIN $CC_CONFIG[showInstances] as si" - ." ON st.instance_id = si.id" - ." LEFT JOIN $CC_CONFIG[showTable] as sh" - ." ON si.show_id = sh.id" - //The next line ensures we only get songs that haven't ended yet - ." WHERE (st.ends >= TIMESTAMP '$p_currentDateTime')" - ." AND (st.ends <= TIMESTAMP '$p_toDateTime')" - //next line makes sure that we aren't returning items that - //are past the show's scheduled timeslot. - ." AND (st.starts < si.ends)" - ." ORDER BY starts"; - * */ + ." ORDER BY start"; + Logging::log($sql); + $rows = $CC_DBC->GetAll($sql); - if (!PEAR::isError($rows)) { - foreach ($rows as &$row) { - $row["start"] = $row["starts"]; - $row["end"] = $row["ends"]; - } + if (PEAR::isError($rows)) { + return null; } - + return $rows; } @@ -508,13 +484,16 @@ class Application_Model_Schedule { $data = array(); $utcTimeZone = new DateTimeZone("UTC"); + $data["status"] = array(); + $data["media"] = array(); + foreach ($items as $item){ $storedFile = Application_Model_StoredFile::Recall($item["file_id"]); $uri = $storedFile->getFileUrlUsingConfigAddress(); $showEndDateTime = new DateTime($item["show_end"], $utcTimeZone); - $trackEndDateTime = new DateTime($item["ends"], $utcTimeZone); + $trackEndDateTime = new DateTime($item["end"], $utcTimeZone); /* Note: cue_out and end are always the same. */ /* TODO: Not all tracks will have "show_end" */ @@ -525,16 +504,16 @@ class Application_Model_Schedule { $item["cue_out"] = $item["cue_out"] - $diff; } - $starts = Application_Model_Schedule::AirtimeTimeToPypoTime($item["starts"]); - $data[$starts] = array( + $start = Application_Model_Schedule::AirtimeTimeToPypoTime($item["start"]); + $data["media"][$start] = array( 'id' => $storedFile->getGunid(), 'uri' => $uri, 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), 'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]), 'cue_in' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_in"]), 'cue_out' => Application_Model_DateHelper::CalculateLengthInSeconds($item["cue_out"]), - 'start' => $starts, - 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["ends"]) + 'start' => $start, + 'end' => Application_Model_Schedule::AirtimeTimeToPypoTime($item["end"]) ); } diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py index c0b64ea01..df5b9cd1f 100755 --- a/python_apps/api_clients/api_client.py +++ b/python_apps/api_clients/api_client.py @@ -261,15 +261,15 @@ class AirTimeApiClient(ApiClientInterface): export_url = export_url.replace('%%api_key%%', self.config["api_key"]) response = "" - status = 0 try: response_json = self.get_response_from_server(export_url) response = json.loads(response_json) - status = response['check'] + success = True except Exception, e: logger.error(e) + success = False - return status, response + return success, response def get_media(self, uri, dst): diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index d0d0e0e32..6241992b6 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -43,6 +43,8 @@ class PypoFetch(Thread): self.fetch_queue = pypoFetch_q self.push_queue = pypoPush_q + self.logger = logging.getLogger(); + self.cache_dir = os.path.join(config["cache_dir"], "scheduler") logger.debug("Cache dir %s", self.cache_dir) try: @@ -52,12 +54,8 @@ class PypoFetch(Thread): except Exception, e: logger.error(e) - - - self.schedule_data = [] - logger = logging.getLogger('fetch') - logger.info("PypoFetch: init complete") + self.logger.info("PypoFetch: init complete") """ Handle a message from RabbitMQ, put it into our yucky global var. @@ -65,53 +63,51 @@ class PypoFetch(Thread): """ def handle_message(self, message): try: - logger = logging.getLogger('fetch') - logger.info("Received event from Pypo Message Handler: %s" % message) + self.logger.info("Received event from Pypo Message Handler: %s" % message) m = json.loads(message) command = m['event_type'] - logger.info("Handling command: " + command) + self.logger.info("Handling command: " + command) if command == 'update_schedule': self.schedule_data = m['schedule'] - self.process_schedule(self.schedule_data, "scheduler", False) + self.process_schedule(self.schedule_data, False) elif command == 'update_stream_setting': - logger.info("Updating stream setting...") + self.logger.info("Updating stream setting...") self.regenerateLiquidsoapConf(m['setting']) elif command == 'update_stream_format': - logger.info("Updating stream format...") + self.logger.info("Updating stream format...") self.update_liquidsoap_stream_format(m['stream_format']) elif command == 'update_station_name': - logger.info("Updating station name...") + self.logger.info("Updating station name...") self.update_liquidsoap_station_name(m['station_name']) elif command == 'cancel_current_show': - logger.info("Cancel current show command received...") + self.logger.info("Cancel current show command received...") self.stop_current_show() except Exception, e: import traceback top = traceback.format_exc() - logger.error('Exception: %s', e) - logger.error("traceback: %s", top) - logger.error("Exception in handling Message Handler message: %s", e) + self.logger.error('Exception: %s', e) + self.logger.error("traceback: %s", top) + self.logger.error("Exception in handling Message Handler message: %s", e) + def stop_current_show(self): - logger = logging.getLogger('fetch') - logger.debug('Notifying Liquidsoap to stop playback.') + self.logger.debug('Notifying Liquidsoap to stop playback.') try: tn = telnetlib.Telnet(LS_HOST, LS_PORT) tn.write('source.skip\n') tn.write('exit\n') tn.read_all() except Exception, e: - logger.debug(e) - logger.debug('Could not connect to liquidsoap') + self.logger.debug(e) + self.logger.debug('Could not connect to liquidsoap') def regenerateLiquidsoapConf(self, setting): - logger = logging.getLogger('fetch') existing = {} # create a temp file fh = open('/etc/airtime/liquidsoap.cfg', 'r') - logger.info("Reading existing config...") + self.logger.info("Reading existing config...") # read existing conf file and build dict while 1: line = fh.readline() @@ -141,7 +137,7 @@ class PypoFetch(Thread): #restart flag restart = False - logger.info("Looking for changes...") + self.logger.info("Looking for changes...") # look for changes for s in setting: if "output_sound_device" in s[u'keyname'] or "icecast_vorbis_metadata" in s[u'keyname']: @@ -149,13 +145,13 @@ class PypoFetch(Thread): state_change_restart[stream] = False # This is the case where restart is required no matter what if (existing[s[u'keyname']] != s[u'value']): - logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) + self.logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) restart = True; else: stream, dump = s[u'keyname'].split('_',1) if "_output" in s[u'keyname']: if (existing[s[u'keyname']] != s[u'value']): - logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) + self.logger.info("'Need-to-restart' state detected for %s...", s[u'keyname']) restart = True; state_change_restart[stream] = True elif ( s[u'value'] != 'disabled'): @@ -167,22 +163,22 @@ class PypoFetch(Thread): if stream not in change: change[stream] = False if not (s[u'value'] == existing[s[u'keyname']]): - logger.info("Keyname: %s, Curent value: %s, New Value: %s", s[u'keyname'], existing[s[u'keyname']], s[u'value']) + self.logger.info("Keyname: %s, Curent value: %s, New Value: %s", s[u'keyname'], existing[s[u'keyname']], s[u'value']) change[stream] = True # set flag change for sound_device alway True - logger.info("Change:%s, State_Change:%s...", change, state_change_restart) + self.logger.info("Change:%s, State_Change:%s...", change, state_change_restart) for k, v in state_change_restart.items(): if k == "sound_device" and v: restart = True elif v and change[k]: - logger.info("'Need-to-restart' state detected for %s...", k) + self.logger.info("'Need-to-restart' state detected for %s...", k) restart = True # rewrite if restart: fh = open('/etc/airtime/liquidsoap.cfg', 'w') - logger.info("Rewriting liquidsoap.cfg...") + self.logger.info("Rewriting liquidsoap.cfg...") fh.write("################################################\n") fh.write("# THIS FILE IS AUTO GENERATED. DO NOT CHANGE!! #\n") fh.write("################################################\n") @@ -204,17 +200,16 @@ class PypoFetch(Thread): fh.close() # restarting pypo. # we could just restart liquidsoap but it take more time somehow. - logger.info("Restarting pypo...") + self.logger.info("Restarting pypo...") sys.exit(0) else: - logger.info("No change detected in setting...") + self.logger.info("No change detected in setting...") self.update_liquidsoap_connection_status() """ updates the status of liquidsoap connection to the streaming server This fucntion updates the bootup time variable in liquidsoap script """ def update_liquidsoap_connection_status(self): - logger = logging.getLogger('fetch') tn = telnetlib.Telnet(LS_HOST, LS_PORT) # update the boot up time of liquidsoap. Since liquidsoap is not restarting, # we are manually adjusting the bootup time variable so the status msg will get @@ -232,7 +227,7 @@ class PypoFetch(Thread): # streamin info is in the form of: # eg. s1:true,2:true,3:false streams = stream_info.split(",") - logger.info(streams) + self.logger.info(streams) fake_time = current_time + 1 for s in streams: @@ -246,33 +241,31 @@ class PypoFetch(Thread): # Push stream metadata to liquidsoap # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!! try: - logger = logging.getLogger('fetch') - logger.info(LS_HOST) - logger.info(LS_PORT) + self.logger.info(LS_HOST) + self.logger.info(LS_PORT) tn = telnetlib.Telnet(LS_HOST, LS_PORT) command = ('vars.stream_metadata_type %s\n' % stream_format).encode('utf-8') - logger.info(command) + self.logger.info(command) tn.write(command) tn.write('exit\n') tn.read_all() except Exception, e: - logger.error("Exception %s", e) + self.logger.error("Exception %s", e) def update_liquidsoap_station_name(self, station_name): # Push stream metadata to liquidsoap # TODO: THIS LIQUIDSOAP STUFF NEEDS TO BE MOVED TO PYPO-PUSH!!! try: - logger = logging.getLogger('fetch') - logger.info(LS_HOST) - logger.info(LS_PORT) + self.logger.info(LS_HOST) + self.logger.info(LS_PORT) tn = telnetlib.Telnet(LS_HOST, LS_PORT) command = ('vars.station_name %s\n' % station_name).encode('utf-8') - logger.info(command) + self.logger.info(command) tn.write(command) tn.write('exit\n') tn.read_all() except Exception, e: - logger.error("Exception %s", e) + self.logger.error("Exception %s", e) """ Process the schedule @@ -283,165 +276,162 @@ class PypoFetch(Thread): - runs the cleanup routine, to get rid of unused cached files """ def process_schedule(self, schedule_data, bootstrapping): - logger = logging.getLogger('fetch') - playlists = schedule_data["playlists"] + media = schedule_data["media"] # Download all the media and put playlists in liquidsoap "annotate" format try: - liquidsoap_playlists = self.prepare_playlists(playlists, bootstrapping) - except Exception, e: logger.error("%s", e) + media = self.prepare_media(media, bootstrapping) + except Exception, e: self.logger.error("%s", e) # Send the data to pypo-push scheduled_data = dict() - scheduled_data['liquidsoap_playlists'] = liquidsoap_playlists - scheduled_data['schedule'] = playlists - self.push_queue.put(scheduled_data) + scheduled_data['liquidsoap_annotation_queue'] = liquidsoap_annotation_queue + self.push_queue.put(media) + + """ # cleanup try: self.cleanup() - except Exception, e: logger.error("%s", e) + except Exception, e: self.logger.error("%s", e) + """ - """ - In this function every audio file is cut as necessary (cue_in/cue_out != 0) - and stored in a playlist folder. - file is e.g. 2010-06-23-15-00-00/17_cue_10.132-123.321.mp3 - """ - def prepare_playlists(self, playlists, bootstrapping): - logger = logging.getLogger('fetch') - - liquidsoap_playlists = dict() - - # Dont do anything if playlists is empty - if not playlists: - logger.debug("Schedule is empty.") - return liquidsoap_playlists - - scheduleKeys = sorted(playlists.iterkeys()) + + def prepare_media(self, media, bootstrapping): + """ + Iterate through the list of media items in "media" and + download them. + """ try: - for pkey in scheduleKeys: - logger.info("Playlist starting at %s", pkey) - playlist = playlists[pkey] + mediaKeys = sorted(media.iterkeys()) + for mkey in mediaKeys: + self.logger.debug("Media item starting at %s", mkey) + media_item = media[mkey] + + if bootstrapping: + check_for_crash(media_item) # create playlist directory try: - os.mkdir(self.cache_dir + str(pkey)) + """ + Extract year, month, date from mkey + """ + y_m_d = mkey[0:10] + download_dir = os.mkdir(os.path.join(self.cache_dir, y_m_d)) + fileExt = os.path.splitext(media_item['uri'])[1] + dst = os.path.join(download_dir, media_item['id']+fileExt) except Exception, e: - logger.warning(e) + self.logger.warning(e) + + if self.handle_media_file(media_item, dst): + entry = create_liquidsoap_annotation(media_item, dst) + #entry['show_name'] = playlist['show_name'] + entry['show_name'] = "TODO" + media_item["annotation"] = entry - ls_playlist = self.handle_media_file(playlist, pkey, bootstrapping) - - liquidsoap_playlists[pkey] = ls_playlist except Exception, e: - logger.error("%s", e) - return liquidsoap_playlists + self.logger.error("%s", e) + + return media + + def create_liquidsoap_annotation(media, dst): + pl_entry = \ + 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ + % (media['id'], 0, \ + float(media['fade_in']) / 1000, \ + float(media['fade_out']) / 1000, \ + float(media['cue_in']), \ + float(media['cue_out']), \ + media['row_id'], dst) - """ - Download and cache the media files. - This handles both remote and local files. - Returns an updated ls_playlist string. - """ - def handle_media_file(self, playlist, pkey, bootstrapping): - logger = logging.getLogger('fetch') + """ + Tracks are only added to the playlist if they are accessible + on the file system and larger than 0 bytes. + So this can lead to playlists shorter than expectet. + (there is a hardware silence detector for this cases...) + """ + entry = dict() + entry['type'] = 'file' + entry['annotate'] = pl_entry + return entry - ls_playlist = [] + def check_for_crash(media_item): + start = media_item['start'] + end = media_item['end'] dtnow = datetime.utcnow() str_tnow_s = dtnow.strftime('%Y-%m-%d-%H-%M-%S') - - sortedKeys = sorted(playlist['medias'].iterkeys()) - - for key in sortedKeys: - media = playlist['medias'][key] - logger.debug("Processing track %s", media['uri']) + + if start <= str_tnow_s and str_tnow_s < end: + #song is currently playing and we just started pypo. Maybe there + #was a power outage? Let's restart playback of this song. + start_split = map(int, start.split('-')) + media_start = datetime(start_split[0], start_split[1], start_split[2], start_split[3], start_split[4], start_split[5], 0, None) + self.logger.debug("Found media item that started at %s.", media_start) - if bootstrapping: - start = media['start'] - end = media['end'] - - if end <= str_tnow_s: - continue - elif start <= str_tnow_s and str_tnow_s < end: - #song is currently playing and we just started pypo. Maybe there - #was a power outage? Let's restart playback of this song. - start_split = map(int, start.split('-')) - media_start = datetime(start_split[0], start_split[1], start_split[2], start_split[3], start_split[4], start_split[5], 0, None) - logger.debug("Found media item that started at %s.", media_start) - - delta = dtnow - media_start #we get a TimeDelta object from this operation - logger.info("Starting media item at %d second point", delta.seconds) - media['cue_in'] = delta.seconds + 10 - td = timedelta(seconds=10) - playlist['start'] = (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S') - logger.info("Crash detected, setting playlist to restart at %s", (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S')) + delta = dtnow - media_start #we get a TimeDelta object from this operation + self.logger.info("Starting media item at %d second point", delta.seconds) + """ + Set the cue_in. This is used by Liquidsoap to determine at what point in the media + item it should start playing. If the cue_in happens to be > cue_out, then make cue_in = cue_out + """ + media_item['cue_in'] = delta.seconds + 10 if delta.seconds + 10 < media_item['cue_out'] else media_item['cue_out'] + + """ + Set the start time, which is used by pypo-push to determine when a media item is scheduled. + Pushing the start time into the future will ensure pypo-push will push this to Liquidsoap. + """ + td = timedelta(seconds=10) + media_item['start'] = (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S') + self.logger.info("Crash detected, setting playlist to restart at %s", (dtnow + td).strftime('%Y-%m-%d-%H-%M-%S')) + + def handle_media_file(self, media_item, dst): + """ + Download and cache the media item. + """ + + self.logger.debug("Processing track %s", media_item['uri']) - fileExt = os.path.splitext(media['uri'])[1] - try: - dst = os.path.join(self.cache_dir, pkey, media['id']+fileExt) - - # download media file - self.handle_remote_file(media, dst) - - if True == os.access(dst, os.R_OK): - # check filesize (avoid zero-byte files) - try: fsize = os.path.getsize(dst) - except Exception, e: - logger.error("%s", e) - fsize = 0 - + try: + #blocking function to download the media item + self.download_file(media_item, dst) + + if os.access(dst, os.R_OK): + # check filesize (avoid zero-byte files) + try: + fsize = os.path.getsize(dst) if fsize > 0: - pl_entry = \ - 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ - % (media['id'], 0, \ - float(media['fade_in']) / 1000, \ - float(media['fade_out']) / 1000, \ - float(media['cue_in']), \ - float(media['cue_out']), \ - media['row_id'], dst) + return True + except Exception, e: + self.logger.error("%s", e) + fsize = 0 + else: + self.logger.warning("Cannot read file %s.", dst) - """ - Tracks are only added to the playlist if they are accessible - on the file system and larger than 0 bytes. - So this can lead to playlists shorter than expectet. - (there is a hardware silence detector for this cases...) - """ - entry = dict() - entry['type'] = 'file' - entry['annotate'] = pl_entry - entry['show_name'] = playlist['show_name'] - ls_playlist.append(entry) - - else: - logger.warning("zero-size file - skipping %s. will not add it to playlist at %s", media['uri'], dst) - - else: - logger.warning("something went wrong. file %s not available. will not add it to playlist", dst) - - except Exception, e: logger.info("%s", e) - return ls_playlist + except Exception, e: + self.logger.info("%s", e) + + return False """ Download a file from a remote server and store it in the cache. """ - def handle_remote_file(self, media, dst): - logger = logging.getLogger('fetch') + def download_file(self, media_item, dst): if os.path.isfile(dst): pass - #logger.debug("file already in cache: %s", dst) + #self.logger.debug("file already in cache: %s", dst) else: - logger.debug("try to download %s", media['uri']) - self.api_client.get_media(media['uri'], dst) + self.logger.debug("try to download %s", media_item['uri']) + self.api_client.get_media(media_item['uri'], dst) """ Cleans up folders in cache_dir. Look for modification date older than "now - CACHE_FOR" and deletes them. """ def cleanup(self): - logger = logging.getLogger('fetch') - offset = 3600 * int(config["cache_for"]) now = time.time() @@ -451,78 +441,44 @@ class PypoFetch(Thread): timestamp = calendar.timegm(time.strptime(dir, "%Y-%m-%d-%H-%M-%S")) if (now - timestamp) > offset: try: - logger.debug('trying to remove %s - timestamp: %s', os.path.join(r, dir), timestamp) + self.logger.debug('trying to remove %s - timestamp: %s', os.path.join(r, dir), timestamp) shutil.rmtree(os.path.join(r, dir)) except Exception, e: - logger.error("%s", e) + self.logger.error("%s", e) pass else: - logger.info('sucessfully removed %s', os.path.join(r, dir)) + self.logger.info('sucessfully removed %s', os.path.join(r, dir)) except Exception, e: - logger.error(e) + self.logger.error(e) def main(self): - logger = logging.getLogger('fetch') - try: os.mkdir(self.cache_dir) except Exception, e: pass # Bootstrap: since we are just starting up, we need to grab the # most recent schedule. After that we can just wait for updates. - status, self.schedule_data = self.api_client.get_schedule() - if status == 1: - logger.info("Bootstrap schedule received: %s", self.schedule_data) - self.process_schedule(self.schedule_data, "scheduler", True) + success, self.schedule_data = self.api_client.get_schedule() + if success: + self.logger.info("Bootstrap schedule received: %s", self.schedule_data) + self.process_schedule(self.schedule_data, True) loops = 1 while True: - logger.info("Loop #%s", loops) + self.logger.info("Loop #%s", loops) try: - try: - """ - our simple_queue.get() requires a timeout, in which case we - fetch the Airtime schedule manually. It is important to fetch - the schedule periodically because if we didn't, we would only - get schedule updates via RabbitMq if the user was constantly - using the Airtime interface. - - If the user is not using the interface, RabbitMq messages are not - sent, and we will have very stale (or non-existent!) data about the - schedule. - - Currently we are checking every 3600 seconds (1 hour) - """ - message = self.fetch_queue.get(block=True, timeout=3600) - self.handle_message(message) - except Empty, e: - """ - Queue timeout. Fetching data manually - """ - raise - except Exception, e: - """ - sleep 5 seconds so that we don't spin inside this - while loop and eat all the CPU - """ - time.sleep(5) - - """ - There is a problem with the RabbitMq messenger service. Let's - log the error and get the schedule via HTTP polling - """ - logger.error("Exception, %s", e) - raise + message = self.fetch_queue.get(block=True, timeout=3600) + self.handle_message(message) except Exception, e: """ There is a problem with the RabbitMq messenger service. Let's log the error and get the schedule via HTTP polling """ - logger.error("Exception, %s", e) + self.logger.error("Exception, %s", e) status, self.schedule_data = self.api_client.get_schedule() if status == 1: - self.process_schedule(self.schedule_data, "scheduler", False) + self.process_schedule(self.schedule_data, False) loops += 1 diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 1d7c132c9..2ecdbbe6e 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -36,11 +36,10 @@ class PypoPush(Thread): self.api_client = api_client.api_client_factory(config) self.queue = q - self.schedule = dict() - self.playlists = dict() + self.media = dict() self.liquidsoap_state_play = True - self.push_ahead = 10 + self.push_ahead = 30 """ The Push Loop - the push loop periodically checks if there is a playlist @@ -56,35 +55,30 @@ class PypoPush(Thread): if not self.queue.empty(): # make sure we get the latest schedule while not self.queue.empty(): - scheduled_data = self.queue.get() - logger.debug("Received data from pypo-fetch") - self.schedule = scheduled_data['schedule'] - self.playlists = scheduled_data['liquidsoap_playlists'] - - logger.debug('schedule %s' % json.dumps(self.schedule)) - logger.debug('playlists %s' % json.dumps(self.playlists)) + self.media = self.queue.get() + logger.debug("Received data from pypo-fetch") + logger.debug('media %s' % json.dumps(self.media)) - schedule = self.schedule - playlists = self.playlists + media = self.media currently_on_air = False - if schedule: + if media: tnow = time.gmtime(timenow) tcoming = time.gmtime(timenow + self.push_ahead) str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5]) str_tcoming_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcoming[0], tcoming[1], tcoming[2], tcoming[3], tcoming[4], tcoming[5]) - for pkey in schedule: - plstart = schedule[pkey]['start'][0:19] - - if str_tnow_s <= plstart and plstart < str_tcoming_s: - logger.debug('Preparing to push playlist scheduled at: %s', pkey) - playlist = schedule[pkey] - - - # We have a match, replace the current playlist and - # force liquidsoap to refresh. - if (self.push_liquidsoap(pkey, schedule, playlists) == 1): + + for media_item in media: + item_start = media_item['start'][0:19] + + if str_tnow_s <= item_start and item_start < str_tcoming_s: + """ + If the media item starts in the next 30 seconds, push it to the queue. + """ + logger.debug('Preparing to push media item scheduled at: %s', pkey) + + if self.push_to_liquidsoap(media_item): logger.debug("Pushed to liquidsoap, updating 'played' status.") currently_on_air = True @@ -93,33 +87,31 @@ class PypoPush(Thread): # Call API to update schedule states logger.debug("Doing callback to server to update 'played' status.") self.api_client.notify_scheduled_item_start_playing(pkey, schedule) + - show_start = schedule[pkey]['show_start'] - show_end = schedule[pkey]['show_end'] - - if show_start <= str_tnow_s and str_tnow_s < show_end: - currently_on_air = True + def push_to_liquidsoap(self, media_item): + if media_item["starts"] == self.last_end_time: """ - If currently_on_air = False but liquidsoap_state_play = True then it means that Liquidsoap may - still be playing audio even though the show has ended ('currently_on_air = False' means no show is scheduled) - See CC-3231. - This is a temporary solution for Airtime 2.0 - """ - if not currently_on_air and self.liquidsoap_state_play: - logger.debug('Notifying Liquidsoap to stop playback.') - try: - tn = telnetlib.Telnet(LS_HOST, LS_PORT) - tn.write('source.skip\n') - tn.write('exit\n') - tn.read_all() - except Exception, e: - logger.debug(e) - logger.debug('Could not connect to liquidsoap') + this media item is attached to the end of the last + track, so let's push it now so that Liquidsoap can start playing + it immediately after (and prepare crossfades if need be). + """ + tn = telnetlib.Telnet(LS_HOST, LS_PORT) + tn.write(str('queue.push %s\n' % media_item["annotation"].encode('utf-8'))) + #TODO: vars.pypo_data + #TODO: vars.show_name + tn.write("exit\n") + + self.last_end_time = media_item["end"] + else: + """ + this media item does not start right after a current playing track. + We need to sleep, and then wake up when this track starts. + """ + + return False - self.liquidsoap_state_play = False - - - def push_liquidsoap(self, pkey, schedule, playlists): + def push_liquidsoap_old(self, pkey, schedule, playlists): logger = logging.getLogger('push') try: @@ -127,10 +119,6 @@ class PypoPush(Thread): plstart = schedule[pkey]['start'][0:19] #strptime returns struct_time in local time - #mktime takes a time_struct and returns a floating point - #gmtime Convert a time expressed in seconds since the epoch to a struct_time in UTC - #mktime: expresses the time in local time, not UTC. It returns a floating point number, for compatibility with time(). - epoch_start = calendar.timegm(time.strptime(plstart, '%Y-%m-%d-%H-%M-%S')) #Return the time as a floating point number expressed in seconds since the epoch, in UTC. @@ -186,7 +174,7 @@ class PypoPush(Thread): if loops % heartbeat_period == 0: logger.info("heartbeat") loops = 0 - try: self.push('scheduler') + try: self.push() except Exception, e: logger.error('Pypo Push Exception: %s', e) time.sleep(PUSH_INTERVAL) From 1d02c56874ace9178f7247385422768bed20ed78 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Tue, 28 Feb 2012 11:06:31 -0500 Subject: [PATCH 17/39] CC-3336: Refactor schedule API used by pypo -refactored pypo-push --- python_apps/pypo/pypopush.py | 129 +++++++++++++++-------------------- 1 file changed, 55 insertions(+), 74 deletions(-) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 2ecdbbe6e..a737d43be 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -87,84 +87,65 @@ class PypoPush(Thread): # Call API to update schedule states logger.debug("Doing callback to server to update 'played' status.") self.api_client.notify_scheduled_item_start_playing(pkey, schedule) - - + def push_to_liquidsoap(self, media_item): - if media_item["starts"] == self.last_end_time: - """ - this media item is attached to the end of the last - track, so let's push it now so that Liquidsoap can start playing - it immediately after (and prepare crossfades if need be). - """ - tn = telnetlib.Telnet(LS_HOST, LS_PORT) - tn.write(str('queue.push %s\n' % media_item["annotation"].encode('utf-8'))) - #TODO: vars.pypo_data - #TODO: vars.show_name - tn.write("exit\n") - - self.last_end_time = media_item["end"] - else: - """ - this media item does not start right after a current playing track. - We need to sleep, and then wake up when this track starts. - """ - - return False - - def push_liquidsoap_old(self, pkey, schedule, playlists): - logger = logging.getLogger('push') - try: - playlist = playlists[pkey] - plstart = schedule[pkey]['start'][0:19] - - #strptime returns struct_time in local time - epoch_start = calendar.timegm(time.strptime(plstart, '%Y-%m-%d-%H-%M-%S')) - - #Return the time as a floating point number expressed in seconds since the epoch, in UTC. - epoch_now = time.time() - - logger.debug("Epoch start: %s" % epoch_start) - logger.debug("Epoch now: %s" % epoch_now) - - sleep_time = epoch_start - epoch_now; - - if sleep_time < 0: - sleep_time = 0 - - logger.debug('sleeping for %s s' % (sleep_time)) - time.sleep(sleep_time) - - tn = telnetlib.Telnet(LS_HOST, LS_PORT) - - #skip the currently playing song if any. - logger.debug("source.skip\n") - tn.write("source.skip\n") - - # Get any extra information for liquidsoap (which will be sent back to us) - liquidsoap_data = self.api_client.get_liquidsoap_data(pkey, schedule) - - #Sending schedule table row id string. - logger.debug("vars.pypo_data %s\n"%(liquidsoap_data["schedule_id"])) - tn.write(("vars.pypo_data %s\n"%liquidsoap_data["schedule_id"]).encode('utf-8')) - - logger.debug('Preparing to push playlist %s' % pkey) - for item in playlist: - annotate = item['annotate'] - tn.write(str('queue.push %s\n' % annotate.encode('utf-8'))) - - show_name = item['show_name'] - tn.write(str('vars.show_name %s\n' % show_name.encode('utf-8'))) - - tn.write("exit\n") - logger.debug(tn.read_all()) - - status = 1 + if media_item["starts"] == self.last_end_time: + """ + this media item is attached to the end of the last + track, so let's push it now so that Liquidsoap can start playing + it immediately after (and prepare crossfades if need be). + """ + telnet_to_liquidsoap(media_item) + self.last_end_time = media_item["end"] + else: + """ + this media item does not start right after a current playing track. + We need to sleep, and then wake up when this track starts. + """ + sleep_until_start(media_item) + + telnet_to_liquidsoap(media_item) + self.last_end_time = media_item["end"] except Exception, e: - logger.error('%s', e) - status = 0 - return status + return False + + return True + def sleep_until_start(media_item): + mi_start = media_item['start'][0:19] + + #strptime returns struct_time in local time + epoch_start = calendar.timegm(time.strptime(mi_start, '%Y-%m-%d-%H-%M-%S')) + + #Return the time as a floating point number expressed in seconds since the epoch, in UTC. + epoch_now = time.time() + + logger.debug("Epoch start: %s" % epoch_start) + logger.debug("Epoch now: %s" % epoch_now) + + sleep_time = epoch_start - epoch_now + + if sleep_time < 0: + sleep_time = 0 + + logger.debug('sleeping for %s s' % (sleep_time)) + time.sleep(sleep_time) + + def telnet_to_liquidsoap(media_item): + tn = telnetlib.Telnet(LS_HOST, LS_PORT) + + #tn.write(("vars.pypo_data %s\n"%liquidsoap_data["schedule_id"]).encode('utf-8')) + + annotation = media_item['annotation'] + tn.write('queue.push %s\n' % annotation.encode('utf-8')) + + show_name = media_item['show_name'] + tn.write('vars.show_name %s\n' % show_name.encode('utf-8')) + + tn.write("exit\n") + logger.debug(tn.read_all()) + def run(self): loops = 0 heartbeat_period = math.floor(30/PUSH_INTERVAL) From b572b26b685f972e94364d0f5eecc83950ac3c8e Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Tue, 28 Feb 2012 15:32:18 -0500 Subject: [PATCH 18/39] CC-3336: Refactor schedule API used by pypo -make sure that empty arrays are objects and not arrays -clean up some comments --- .../application/controllers/ApiController.php | 4 +- python_apps/pypo/pypofetch.py | 44 +++++++++---------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index b82366e08..20e9a0f0d 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -287,8 +287,8 @@ class ApiController extends Zend_Controller_Action PEAR::setErrorHandling(PEAR_ERROR_RETURN); - $result = Application_Model_Schedule::GetScheduledPlaylists(); - echo json_encode($result); + $data = Application_Model_Schedule::GetScheduledPlaylists(); + echo json_encode($data, JSON_FORCE_OBJECT); } public function notifyMediaItemStartPlayAction() diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 6241992b6..723f95a7a 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -48,11 +48,16 @@ class PypoFetch(Thread): self.cache_dir = os.path.join(config["cache_dir"], "scheduler") logger.debug("Cache dir %s", self.cache_dir) try: - if not os.path.exists(dir): + if not os.path.isdir(dir): + """ + We get here if path does not exist, or path does exist but + is a file. We are not handling the second case, but don't + think we actually care about handling it. + """ logger.debug("Cache dir does not exist. Creating...") os.makedirs(dir) except Exception, e: - logger.error(e) + pass self.schedule_data = [] self.logger.info("PypoFetch: init complete") @@ -276,6 +281,9 @@ class PypoFetch(Thread): - runs the cleanup routine, to get rid of unused cached files """ def process_schedule(self, schedule_data, bootstrapping): + + self.logger.debug(schedule_data) + media = schedule_data["media"] # Download all the media and put playlists in liquidsoap "annotate" format @@ -284,12 +292,12 @@ class PypoFetch(Thread): except Exception, e: self.logger.error("%s", e) # Send the data to pypo-push - scheduled_data = dict() - - scheduled_data['liquidsoap_annotation_queue'] = liquidsoap_annotation_queue + + self.logger.debug("Pushing to pypo-push: "+ str(media)) self.push_queue.put(media) """ + TODO # cleanup try: self.cleanup() except Exception, e: self.logger.error("%s", e) @@ -309,7 +317,7 @@ class PypoFetch(Thread): media_item = media[mkey] if bootstrapping: - check_for_crash(media_item) + check_for_previous_crash(media_item) # create playlist directory try: @@ -356,7 +364,7 @@ class PypoFetch(Thread): entry['annotate'] = pl_entry return entry - def check_for_crash(media_item): + def check_for_previous_crash(media_item): start = media_item['start'] end = media_item['end'] @@ -453,9 +461,6 @@ class PypoFetch(Thread): def main(self): - try: os.mkdir(self.cache_dir) - except Exception, e: pass - # Bootstrap: since we are just starting up, we need to grab the # most recent schedule. After that we can just wait for updates. success, self.schedule_data = self.api_client.get_schedule() @@ -470,23 +475,16 @@ class PypoFetch(Thread): message = self.fetch_queue.get(block=True, timeout=3600) self.handle_message(message) except Exception, e: - """ - There is a problem with the RabbitMq messenger service. Let's - log the error and get the schedule via HTTP polling - """ self.logger.error("Exception, %s", e) - status, self.schedule_data = self.api_client.get_schedule() - if status == 1: + success, self.schedule_data = self.api_client.get_schedule() + if success: self.process_schedule(self.schedule_data, False) loops += 1 - """ - Main loop of the thread: - Wait for schedule updates from RabbitMQ, but in case there arent any, - poll the server to get the upcoming schedule. - """ def run(self): - while True: - self.main() + """ + Entry point of the thread + """ + self.main() From 5257711866701032c283bc1898bee962ff42955f Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Tue, 28 Feb 2012 23:52:20 +0100 Subject: [PATCH 19/39] CC-3174 : showbuilder fixing order of scheduled/playlist data when added via group dragging. --- .../controllers/PlaylistController.php | 2 +- .../library/events/library_playlistbuilder.js | 11 ++++---- airtime_mvc/public/js/airtime/library/spl.js | 25 ++++++++++++------- .../public/js/airtime/showbuilder/builder.js | 19 +++++++------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php index b40de9d35..efa4fb8b4 100644 --- a/airtime_mvc/application/controllers/PlaylistController.php +++ b/airtime_mvc/application/controllers/PlaylistController.php @@ -197,7 +197,7 @@ class PlaylistController extends Zend_Controller_Action public function addItemsAction() { - $ids = $this->_getParam('ids'); + $ids = $this->_getParam('ids', array()); $ids = (!is_array($ids)) ? array($ids) : $ids; $afterItem = $this->_getParam('afterItem', null); $addType = $this->_getParam('type', 'after'); diff --git a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js index 676bac86c..3f0216faf 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js @@ -69,16 +69,17 @@ var AIRTIME = (function(AIRTIME){ fnAddSelectedItems = function() { var oLibTT = TableTools.fnGetInstance('library_display'), aData = oLibTT.fnGetSelectedData(), - item, + i, temp, + length, aMediaIds = []; //process selected files/playlists. - for (item in aData) { - temp = aData[item]; - if (temp !== null && temp.hasOwnProperty('id') && temp.ftype === "audioclip") { + for (i = 0, length = aData.length; i < length; i++) { + temp = aData[i]; + if (temp.ftype === "audioclip") { aMediaIds.push(temp.id); - } + } } AIRTIME.playlist.fnAddItems(aMediaIds, undefined, 'after'); diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index 4ca90ee9a..e90aeaa33 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -506,18 +506,25 @@ var AIRTIME = (function(AIRTIME){ fnUpdate; fnReceive = function(event, ui) { - var selected = $('#library_display tr[id^="au"] input:checked').parents('tr'), - aItems = []; - + var aItems = [], + aSelected, + oLibTT = TableTools.fnGetInstance('library_display'), + i, + length; + + //filter out anything that isn't an audiofile. + aSelected = oLibTT.fnGetSelectedData(); //if nothing is checked select the dragged item. - if (selected.length === 0) { - selected = ui.item; + if (aSelected.length === 0) { + aSelected.push(ui.item.data("aData")); } - selected.each(function(i, el) { - aItems.push($(el).data("aData").id); - }); - + for (i = 0, length = aSelected.length; i < length; i++) { + if (aSelected[i].ftype === "audioclip") { + aItems.push(aSelected[i].id); + } + } + aReceiveItems = aItems; html = ui.helper.html(); }; diff --git a/airtime_mvc/public/js/airtime/showbuilder/builder.js b/airtime_mvc/public/js/airtime/showbuilder/builder.js index 577b6a418..85c2e2932 100644 --- a/airtime_mvc/public/js/airtime/showbuilder/builder.js +++ b/airtime_mvc/public/js/airtime/showbuilder/builder.js @@ -499,8 +499,7 @@ $(document).ready(function() { fnAdd = function() { var aMediaIds = [], - aSchedIds = [], - oLibTT = TableTools.fnGetInstance('library_display'); + aSchedIds = []; for(i=0; i < aItemData.length; i++) { aMediaIds.push({"id": aItemData[i].id, "type": aItemData[i].ftype}); @@ -521,18 +520,18 @@ $(document).ready(function() { }; fnReceive = function(event, ui) { - var selected = $('#library_display tr:not(:first) input:checked').parents('tr'), - aItems = []; + var aItems = [], + oLibTT = TableTools.fnGetInstance('library_display'), + i, + length; + + aItems = oLibTT.fnGetSelectedData(); //if nothing is checked select the dragged item. - if (selected.length === 0) { - selected = ui.item; + if (aItems.length === 0) { + aItems.push(ui.item.data("aData")); } - selected.each(function(i, el) { - aItems.push($(el).data("aData")); - }); - origTrs = aItems; html = ui.helper.html(); }; From a6413f2d1ab5adafac348ba04a1934266750069f Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Tue, 28 Feb 2012 22:33:19 -0500 Subject: [PATCH 20/39] CC-3336: Refactor schedule API used by pypo --- airtime_mvc/application/models/RabbitMq.php | 2 +- airtime_mvc/application/models/Schedule.php | 6 ++++++ python_apps/pypo/pypo-cli.py | 5 ++++- python_apps/pypo/pypofetch.py | 20 ++++++++++++-------- python_apps/pypo/pypopush.py | 19 ++++++++++--------- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/airtime_mvc/application/models/RabbitMq.php b/airtime_mvc/application/models/RabbitMq.php index 4d0e501a5..a01d5c9a9 100644 --- a/airtime_mvc/application/models/RabbitMq.php +++ b/airtime_mvc/application/models/RabbitMq.php @@ -29,7 +29,7 @@ class Application_Model_RabbitMq $EXCHANGE = 'airtime-pypo'; $channel->exchange_declare($EXCHANGE, 'direct', false, true); - $data = json_encode($md); + $data = json_encode($md, JSON_FORCE_OBJECT); $msg = new AMQPMessage($data, array('content_type' => 'text/plain')); $channel->basic_publish($msg, $EXCHANGE); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index f1583bb00..af0d0fc30 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -423,8 +423,13 @@ class Application_Model_Schedule { $rows = array(); $sql = "SELECT st.file_id AS file_id," + ." st.id as 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" ." FROM $CC_CONFIG[scheduleTable] as st" @@ -491,6 +496,7 @@ class Application_Model_Schedule { $start = Application_Model_Schedule::AirtimeTimeToPypoTime($item["start"]); $data["media"][$start] = array( 'id' => $storedFile->getGunid(), + 'row_id' => $item["id"], 'uri' => $uri, 'fade_in' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_in"]), 'fade_out' => Application_Model_Schedule::WallTimeToMillisecs($item["fade_out"]), diff --git a/python_apps/pypo/pypo-cli.py b/python_apps/pypo/pypo-cli.py index 8bfd7d026..300f57f9c 100644 --- a/python_apps/pypo/pypo-cli.py +++ b/python_apps/pypo/pypo-cli.py @@ -141,8 +141,11 @@ if __name__ == '__main__': recorder.daemon = True recorder.start() - #pp.join() + pmh.join() + pp.join() pf.join() + recorder.join() + logger.info("pypo fetch exit") sys.exit() """ diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 765702f01..99a06d6d5 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -45,7 +45,7 @@ class PypoFetch(Thread): self.logger = logging.getLogger(); self.cache_dir = os.path.join(config["cache_dir"], "scheduler") - logger.debug("Cache dir %s", self.cache_dir) + self.logger.debug("Cache dir %s", self.cache_dir) try: if not os.path.isdir(dir): @@ -54,7 +54,7 @@ class PypoFetch(Thread): is a file. We are not handling the second case, but don't think we actually care about handling it. """ - logger.debug("Cache dir does not exist. Creating...") + self.logger.debug("Cache dir does not exist. Creating...") os.makedirs(dir) except Exception, e: pass @@ -210,6 +210,7 @@ class PypoFetch(Thread): else: self.logger.info("No change detected in setting...") self.update_liquidsoap_connection_status() + def update_liquidsoap_connection_status(self): """ updates the status of liquidsoap connection to the streaming server @@ -315,7 +316,7 @@ class PypoFetch(Thread): media_item = media[mkey] if bootstrapping: - check_for_previous_crash(media_item) + self.check_for_previous_crash(media_item) # create playlist directory try: @@ -323,15 +324,18 @@ class PypoFetch(Thread): Extract year, month, date from mkey """ y_m_d = mkey[0:10] - download_dir = os.mkdir(os.path.join(self.cache_dir, y_m_d)) + download_dir = os.path.join(self.cache_dir, y_m_d) + try: + os.makedirs(os.path.join(self.cache_dir, y_m_d)) + except Exception, e: + pass fileExt = os.path.splitext(media_item['uri'])[1] dst = os.path.join(download_dir, media_item['id']+fileExt) except Exception, e: self.logger.warning(e) if self.handle_media_file(media_item, dst): - entry = create_liquidsoap_annotation(media_item, dst) - #entry['show_name'] = playlist['show_name'] + entry = self.create_liquidsoap_annotation(media_item, dst) entry['show_name'] = "TODO" media_item["annotation"] = entry @@ -341,7 +345,7 @@ class PypoFetch(Thread): return media - def create_liquidsoap_annotation(media, dst): + def create_liquidsoap_annotation(self, media, dst): pl_entry = \ 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ % (media['id'], 0, \ @@ -362,7 +366,7 @@ class PypoFetch(Thread): entry['annotate'] = pl_entry return entry - def check_for_previous_crash(media_item): + def check_for_previous_crash(self, media_item): start = media_item['start'] end = media_item['end'] diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 0d8f8091e..aaddb69da 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -40,6 +40,7 @@ class PypoPush(Thread): self.liquidsoap_state_play = True self.push_ahead = 30 + self.last_end_time = 0 def push(self): """ @@ -68,15 +69,15 @@ class PypoPush(Thread): str_tnow_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tnow[0], tnow[1], tnow[2], tnow[3], tnow[4], tnow[5]) str_tcoming_s = "%04d-%02d-%02d-%02d-%02d-%02d" % (tcoming[0], tcoming[1], tcoming[2], tcoming[3], tcoming[4], tcoming[5]) - - for media_item in media: + for key in media: + media_item = media[key] item_start = media_item['start'][0:19] if str_tnow_s <= item_start and item_start < str_tcoming_s: """ If the media item starts in the next 30 seconds, push it to the queue. """ - logger.debug('Preparing to push media item scheduled at: %s', pkey) + logger.debug('Preparing to push media item scheduled at: %s', key) if self.push_to_liquidsoap(media_item): logger.debug("Pushed to liquidsoap, updating 'played' status.") @@ -86,7 +87,7 @@ class PypoPush(Thread): # Call API to update schedule states logger.debug("Doing callback to server to update 'played' status.") - self.api_client.notify_scheduled_item_start_playing(pkey, schedule) + self.api_client.notify_scheduled_item_start_playing(key, schedule) def push_to_liquidsoap(self, media_item): """ @@ -95,7 +96,7 @@ class PypoPush(Thread): media item before pushing it. """ try: - if media_item["starts"] == self.last_end_time: + if media_item["start"] == self.last_end_time: """ this media item is attached to the end of the last track, so let's push it now so that Liquidsoap can start playing @@ -108,16 +109,16 @@ class PypoPush(Thread): this media item does not start right after a current playing track. We need to sleep, and then wake up when this track starts. """ - sleep_until_start(media_item) + self.sleep_until_start(media_item) - telnet_to_liquidsoap(media_item) + self.telnet_to_liquidsoap(media_item) self.last_end_time = media_item["end"] except Exception, e: return False return True - def sleep_until_start(media_item): + def sleep_until_start(self, media_item): """ The purpose of this function is to look at the difference between "now" and when the media_item starts, and sleep for that period of time. @@ -143,7 +144,7 @@ class PypoPush(Thread): logger.debug('sleeping for %s s' % (sleep_time)) time.sleep(sleep_time) - def telnet_to_liquidsoap(media_item): + def telnet_to_liquidsoap(self, media_item): """ telnets to liquidsoap and pushes the media_item to its queue. Push the show name of every media_item as well, just to keep Liquidsoap up-to-date From 0f91f91c41ff27b0b56722d6a64b3ac2c78fd41a Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Wed, 29 Feb 2012 15:47:11 +0100 Subject: [PATCH 21/39] CC-3174 : showbuilder adding checks to enable/disable buttons for playlist & timeline. --- .../controllers/LibraryController.php | 1 + .../public/js/airtime/buttons/buttons.js | 29 ++++++++ .../library/events/library_playlistbuilder.js | 23 ++++++- .../library/events/library_showbuilder.js | 32 ++++++--- .../public/js/airtime/library/library.js | 69 ++++++++++++------- .../public/js/airtime/showbuilder/builder.js | 36 ++++++---- 6 files changed, 142 insertions(+), 48 deletions(-) create mode 100644 airtime_mvc/public/js/airtime/buttons/buttons.js diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index dd16c377c..772b83a4d 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -55,6 +55,7 @@ class LibraryController extends Zend_Controller_Action $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.FixedColumns.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.TableTools.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/airtime/buttons/buttons.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/library.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headLink()->appendStylesheet($baseUrl.'/css/media_library.css?'.$CC_CONFIG['airtime_version']); diff --git a/airtime_mvc/public/js/airtime/buttons/buttons.js b/airtime_mvc/public/js/airtime/buttons/buttons.js new file mode 100644 index 000000000..e11814aae --- /dev/null +++ b/airtime_mvc/public/js/airtime/buttons/buttons.js @@ -0,0 +1,29 @@ +var AIRTIME = (function(AIRTIME){ + var mod, + DEFAULT_CLASS = 'ui-button ui-state-default', + DISABLED_CLASS = 'ui-state-disabled'; + + if (AIRTIME.button === undefined) { + AIRTIME.button = {}; + } + mod = AIRTIME.button; + + mod.enableButton = function(c) { + var button = $("."+c).find("button"); + + if (button.hasClass(DISABLED_CLASS)) { + button.removeClass(DISABLED_CLASS); + } + }; + + mod.disableButton = function(c) { + var button = $("."+c).find("button"); + + if (!button.hasClass(DISABLED_CLASS)) { + button.addClass(DISABLED_CLASS); + } + }; + + return AIRTIME; + +}(AIRTIME || {})); \ No newline at end of file diff --git a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js index 3f0216faf..0178d5688 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js @@ -7,6 +7,24 @@ var AIRTIME = (function(AIRTIME){ AIRTIME.library.events = {}; mod = AIRTIME.library.events; + + mod.enableAddButtonCheck = function() { + var selected = $('#library_display tr[id ^= "au"] input[type=checkbox]').filter(":checked"), + sortable = $('#spl_sortable'), + check = false; + + //make sure audioclips are selected and a playlist is currently open. + if (selected.length !== 0 && sortable.length !== 0) { + check = true; + } + + if (check === true) { + AIRTIME.button.enableButton("library_group_add"); + } + else { + AIRTIME.button.disableButton("library_group_add"); + } + }; mod.fnRowCallback = function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { var $nRow = $(nRow); @@ -63,7 +81,6 @@ var AIRTIME = (function(AIRTIME){ */ mod.setupLibraryToolbar = function( oLibTable ) { var aButtons, - fnResetCol, fnAddSelectedItems; fnAddSelectedItems = function() { @@ -89,8 +106,8 @@ var AIRTIME = (function(AIRTIME){ //[1] = id //[2] = enabled //[3] = click event - aButtons = [["Delete", "library_group_delete", true, AIRTIME.library.fnDeleteSelectedItems], - ["Add", "library_group_add", true, fnAddSelectedItems]]; + aButtons = [["Delete", "library_group_delete", false, AIRTIME.library.fnDeleteSelectedItems], + ["Add", "library_group_add", false, fnAddSelectedItems]]; addToolBarButtonsLibrary(aButtons); }; diff --git a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js index 822b2f82f..c18d36b9e 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js @@ -8,6 +8,24 @@ var AIRTIME = (function(AIRTIME){ AIRTIME.library.events = {}; mod = AIRTIME.library.events; + mod.enableAddButtonCheck = function() { + var selected = $('#library_display tr input[type=checkbox]').filter(":checked"), + cursor = $('tr.cursor-selected-row'), + check = false; + + //make sure library items are selected and a cursor is selected. + if (selected.length !== 0 && cursor.length !== 0) { + check = true; + } + + if (check === true) { + AIRTIME.button.enableButton("library_group_add"); + } + else { + AIRTIME.button.disableButton("library_group_add"); + } + }; + mod.fnRowCallback = function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { var $nRow = $(nRow); @@ -21,7 +39,6 @@ var AIRTIME = (function(AIRTIME){ $('#library_display tr:not(:first)').draggable({ helper: function(){ var selected = $('#library_display tr:not(:first) input:checked').parents('tr'), - aItems = [], container, thead = $("#show_builder_table thead"), colspan = thead.find("th").length, @@ -34,10 +51,10 @@ var AIRTIME = (function(AIRTIME){ } if (selected.length === 1) { - message = "Moving "+selected.length+" Item." + message = "Moving "+selected.length+" Item."; } else { - message = "Moving "+selected.length+" Items." + message = "Moving "+selected.length+" Items."; } container = $('
').attr('id', 'draggingContainer') @@ -61,8 +78,6 @@ var AIRTIME = (function(AIRTIME){ mod.setupLibraryToolbar = function(oLibTable) { var aButtons, - fnTest, - fnResetCol, fnAddSelectedItems, fnAddSelectedItems = function() { @@ -75,7 +90,7 @@ var AIRTIME = (function(AIRTIME){ aSchedIds = []; //process selected files/playlists. - for (i=0, length = aData.length; i < length; i++) { + for (i = 0, length = aData.length; i < length; i++) { temp = aData[i]; aMediaIds.push({"id": temp.id, "type": temp.ftype}); } @@ -93,12 +108,13 @@ var AIRTIME = (function(AIRTIME){ AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds); }; + //[0] = button text //[1] = id //[2] = enabled //[3] = click event - aButtons = [["Delete", "library_group_delete", true, AIRTIME.library.fnDeleteSelectedItems], - ["Add", "library_group_add", true, fnAddSelectedItems]]; + aButtons = [["Delete", "library_group_delete", false, AIRTIME.library.fnDeleteSelectedItems], + ["Add", "library_group_add", false, fnAddSelectedItems]]; addToolBarButtonsLibrary(aButtons); }; diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 626de54c0..5953a9cca 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -43,42 +43,45 @@ var AIRTIME = (function(AIRTIME){ function addToolBarButtonsLibrary(aButtons) { var i, length = aButtons.length, - libToolBar, + libToolBar = $(".library_toolbar"), html, buttonClass = '', DEFAULT_CLASS = 'ui-button ui-state-default', - DISABLED_CLASS = 'ui-state-disabled'; + DISABLED_CLASS = 'ui-state-disabled', + fn; - libToolBar = $(".library_toolbar"); - - for ( i=0; i < length; i+=1 ) { + for ( i = 0; i < length; i += 1 ) { buttonClass = ''; //add disabled class if not enabled. if (aButtons[i][2] === false) { - buttonClass+=DISABLED_CLASS; + buttonClass += DISABLED_CLASS; } - html = '
'; + html = '
'; libToolBar.append(html); - libToolBar.find("#"+aButtons[i][1]).click(aButtons[i][3]); + + //create a closure to preserve the state of i. + (function(index){ + + libToolBar.find("."+aButtons[index][1]).click(function(){ + fn = function() { + var $button = $(this).find("button"); + + //only call the passed function if the button is enabled. + if (!$button.hasClass(DISABLED_CLASS)) { + aButtons[index][3](); + } + }; + + fn.call(this); + }); + + }(i)); + } } -function enableGroupBtn(btnId, func) { - btnId = '#' + btnId; - if ($(btnId).hasClass('ui-state-disabled')) { - $(btnId).removeClass('ui-state-disabled'); - } -} - -function disableGroupBtn(btnId) { - btnId = '#' + btnId; - if (!$(btnId).hasClass('ui-state-disabled')) { - $(btnId).addClass('ui-state-disabled'); - } -} - function checkImportStatus(){ $.getJSON('/Preference/is-import-in-progress', function(data){ var div = $('#import_status'); @@ -400,24 +403,40 @@ $(document).ready(function() { "sRowSelect": "multi", "aButtons": [], "fnRowSelected": function ( node ) { + var selected; //seems to happen if everything is selected if ( node === null) { - oTable.find("input[type=checkbox]").attr("checked", true); + selected = oTable.find("input[type=checkbox]"); + selected.attr("checked", true); } else { $(node).find("input[type=checkbox]").attr("checked", true); + selected = oTable.find("input[type=checkbox]").filter(":checked"); } + + //checking to enable buttons + AIRTIME.button.enableButton("library_group_delete"); + AIRTIME.library.events.enableAddButtonCheck(); }, "fnRowDeselected": function ( node ) { + var selected; //seems to happen if everything is deselected if ( node === null) { oTable.find("input[type=checkbox]").attr("checked", false); + selected = []; } else { $(node).find("input[type=checkbox]").attr("checked", false); + selected = oTable.find("input[type=checkbox]").filter(":checked"); } + + //checking to disable buttons + if (selected.length === 0) { + AIRTIME.button.disableButton("library_group_delete"); + } + AIRTIME.library.events.enableAddButtonCheck(); } }, @@ -472,7 +491,7 @@ $(document).ready(function() { ignoreRightClick: true, build: function($el, e) { - var x, request, data, screen, items, callback, $tr; + var data, screen, items, callback, $tr; $tr = $el.parent(); data = $tr.data("aData"); @@ -527,7 +546,7 @@ $(document).ready(function() { media.push({"id": data.id, "type": data.ftype}); $.post(oItems.del.url, {format: "json", media: media }, function(json){ - var oTable, tr; + var oTable; if (json.message) { alert(json.message); diff --git a/airtime_mvc/public/js/airtime/showbuilder/builder.js b/airtime_mvc/public/js/airtime/showbuilder/builder.js index 85c2e2932..b8000fc0c 100644 --- a/airtime_mvc/public/js/airtime/showbuilder/builder.js +++ b/airtime_mvc/public/js/airtime/showbuilder/builder.js @@ -419,16 +419,26 @@ $(document).ready(function() { else { $(node).find("input[type=checkbox]").attr("checked", true); } + + //checking to enable buttons + AIRTIME.button.enableButton("sb_delete"); }, "fnRowDeselected": function ( node ) { - + var selected; + //seems to happen if everything is deselected if ( node === null) { - var oTable = $("#show_builder_table").dataTable(); - oTable.find("input[type=checkbox]").attr("checked", false); + tableDiv.find("input[type=checkbox]").attr("checked", false); + selected = []; } else { $(node).find("input[type=checkbox]").attr("checked", false); + selected = tableDiv.find("input[type=checkbox]").filter(":checked"); + } + + //checking to disable buttons + if (selected.length === 0) { + AIRTIME.button.disableButton("sb_delete"); } } }, @@ -501,7 +511,7 @@ $(document).ready(function() { var aMediaIds = [], aSchedIds = []; - for(i=0; i < aItemData.length; i++) { + for(i = 0; i < aItemData.length; i++) { aMediaIds.push({"id": aItemData[i].id, "type": aItemData[i].ftype}); } aSchedIds.push({"id": oPrevData.id, "instance": oPrevData.instance, "timestamp": oPrevData.timestamp}); @@ -521,9 +531,7 @@ $(document).ready(function() { fnReceive = function(event, ui) { var aItems = [], - oLibTT = TableTools.fnGetInstance('library_display'), - i, - length; + oLibTT = TableTools.fnGetInstance('library_display'); aItems = oLibTT.fnGetSelectedData(); @@ -579,7 +587,7 @@ $(document).ready(function() { tableDiv.sortable(sortableConf); $("#show_builder .fg-toolbar") - .append('
') + .append('
') .click(fnRemoveSelectedItems); //set things like a reference to the table. @@ -587,15 +595,19 @@ $(document).ready(function() { //add event to cursors. tableDiv.find("tbody").on("click", "div.marker", function(event){ - var tr = $(this).parents("tr"); + var tr = $(this).parents("tr"), + cursorSelClass = "cursor-selected-row"; - if (tr.hasClass("cursor-selected-row")) { - tr.removeClass("cursor-selected-row"); + if (tr.hasClass(cursorSelClass)) { + tr.removeClass(cursorSelClass); } else { - tr.addClass("cursor-selected-row"); + tr.addClass(cursorSelClass); } + //check if add button can still be enabled. + AIRTIME.library.events.enableAddButtonCheck(); + return false; }); From bc1d519408169e0cedc96fdb72fdcf6ce20b5f68 Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Wed, 29 Feb 2012 16:02:41 +0100 Subject: [PATCH 22/39] CC-3174 : showbuilder showing message if some files could not be deleted since they were scheduled. --- airtime_mvc/public/js/airtime/library/library.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 5953a9cca..ec49e748b 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -13,6 +13,10 @@ var AIRTIME = (function(AIRTIME){ $.post("/library/delete", {"format": "json", "media": aMedia}, function(json){ + if (json.message !== undefined) { + alert(json.message); + } + oLibTT.fnSelectNone(); oLibTable.fnDraw(); }); From 7c74023ef07becd2212ef5e6038ca6ba3289facb Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Wed, 29 Feb 2012 16:09:39 +0100 Subject: [PATCH 23/39] CC-3174 : showbuilder removing ugly dsahed border, keeping height for the library dragging helper. --- airtime_mvc/public/css/playlist_builder.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/airtime_mvc/public/css/playlist_builder.css b/airtime_mvc/public/css/playlist_builder.css index 30947f291..cef0a7eba 100644 --- a/airtime_mvc/public/css/playlist_builder.css +++ b/airtime_mvc/public/css/playlist_builder.css @@ -457,7 +457,5 @@ div.helper li { } li.spl_empty { - text-align: center; height: 56px; - border:2px dashed black; } \ No newline at end of file From a9e7a70dfb6b00a6ebab4394ecc03ac88d8e1b7c Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Wed, 29 Feb 2012 16:23:41 +0100 Subject: [PATCH 24/39] CC-3174 : showbuilder fix audio preview. --- airtime_mvc/application/views/scripts/playlist/update.phtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml index bd55a9f94..24ddd0882 100644 --- a/airtime_mvc/application/views/scripts/playlist/update.phtml +++ b/airtime_mvc/application/views/scripts/playlist/update.phtml @@ -4,11 +4,11 @@ if (count($items)) : ?> -
  • " unqid=""> +
  • " unqid="">
    ', - 'spl_')"> + 'spl_')">
    From a8f53d169b2ca8df3b57adcf21e6e38b7652c2e6 Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Wed, 29 Feb 2012 17:47:26 +0100 Subject: [PATCH 25/39] CC-3174 : showbuilder sizing bit rate/sample rate in library, text right aligned. --- airtime_mvc/public/css/media_library.css | 5 +++++ airtime_mvc/public/js/airtime/library/library.js | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/airtime_mvc/public/css/media_library.css b/airtime_mvc/public/css/media_library.css index 20b60b4bc..97de2e91c 100644 --- a/airtime_mvc/public/css/media_library.css +++ b/airtime_mvc/public/css/media_library.css @@ -79,3 +79,8 @@ .library_year { text-align: center; } + +.library_sr, +.library_bitrate { + text-align: right; +} diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index ec49e748b..97ceea6b1 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -241,8 +241,8 @@ $(document).ready(function() { /* BPM */ {"sTitle": "BPM", "mDataProp": "bpm", "bSearchable": false, "bVisible": false, "sClass": "library_bpm"}, /* Composer */ {"sTitle": "Composer", "mDataProp": "composer", "bSearchable": false, "bVisible": false, "sClass": "library_composer"}, /* Website */ {"sTitle": "Website", "mDataProp": "info_url", "bSearchable": false, "bVisible": false, "sClass": "library_url"}, - /* Bit Rate */ {"sTitle": "Bit Rate", "mDataProp": "bit_rate", "bSearchable": false, "bVisible": false, "sClass": "library_bitrate"}, - /* Sameple Rate */ {"sTitle": "Sample Rate", "mDataProp": "sample_rate", "bSearchable": false, "bVisible": false, "sClass": "library_sr"}, + /* Bit Rate */ {"sTitle": "Bit Rate", "mDataProp": "bit_rate", "bSearchable": false, "bVisible": false, "sClass": "library_bitrate", "sWidth": "80px"}, + /* Sample Rate */ {"sTitle": "Sample", "mDataProp": "sample_rate", "bSearchable": false, "bVisible": false, "sClass": "library_sr", "sWidth": "80px"}, /* ISRC Number */ {"sTitle": "ISRC", "mDataProp": "isrc_number", "bSearchable": false, "bVisible": false, "sClass": "library_isrc"}, /* Encoded */ {"sTitle": "Encoded", "mDataProp": "encoded_by", "bSearchable": false, "bVisible": false, "sClass": "library_encoded"}, /* Label */ {"sTitle": "Label", "mDataProp": "label", "bSearchable": false, "bVisible": false, "sClass": "library_label"}, From 872bd2784729bf1a30286fd48bf07c01cfd38611 Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Wed, 29 Feb 2012 18:06:53 +0100 Subject: [PATCH 26/39] CC-3174 : showbuilder getting rid of useless views. --- .../application/views/scripts/library/contents.phtml | 0 .../application/views/scripts/library/context-menu.phtml | 0 .../application/views/scripts/library/delete.phtml | 1 - .../views/scripts/library/libraryTablePartial.phtml | 8 -------- .../application/views/scripts/library/search.phtml | 1 - .../application/views/scripts/library/update.phtml | 3 --- 6 files changed, 13 deletions(-) delete mode 100644 airtime_mvc/application/views/scripts/library/contents.phtml delete mode 100644 airtime_mvc/application/views/scripts/library/context-menu.phtml delete mode 100644 airtime_mvc/application/views/scripts/library/delete.phtml delete mode 100644 airtime_mvc/application/views/scripts/library/libraryTablePartial.phtml delete mode 100644 airtime_mvc/application/views/scripts/library/search.phtml delete mode 100644 airtime_mvc/application/views/scripts/library/update.phtml diff --git a/airtime_mvc/application/views/scripts/library/contents.phtml b/airtime_mvc/application/views/scripts/library/contents.phtml deleted file mode 100644 index e69de29bb..000000000 diff --git a/airtime_mvc/application/views/scripts/library/context-menu.phtml b/airtime_mvc/application/views/scripts/library/context-menu.phtml deleted file mode 100644 index e69de29bb..000000000 diff --git a/airtime_mvc/application/views/scripts/library/delete.phtml b/airtime_mvc/application/views/scripts/library/delete.phtml deleted file mode 100644 index 5cbb9a545..000000000 --- a/airtime_mvc/application/views/scripts/library/delete.phtml +++ /dev/null @@ -1 +0,0 @@ -

    View script for controller Library and script/action name delete
    \ No newline at end of file diff --git a/airtime_mvc/application/views/scripts/library/libraryTablePartial.phtml b/airtime_mvc/application/views/scripts/library/libraryTablePartial.phtml deleted file mode 100644 index 52074462d..000000000 --- a/airtime_mvc/application/views/scripts/library/libraryTablePartial.phtml +++ /dev/null @@ -1,8 +0,0 @@ - - - track_title ?> - artist_name ?> - album_title ?> - track_number ?> - length ?> - diff --git a/airtime_mvc/application/views/scripts/library/search.phtml b/airtime_mvc/application/views/scripts/library/search.phtml deleted file mode 100644 index 5fb9621a7..000000000 --- a/airtime_mvc/application/views/scripts/library/search.phtml +++ /dev/null @@ -1 +0,0 @@ -

    View script for controller Library and script/action name search
    \ No newline at end of file diff --git a/airtime_mvc/application/views/scripts/library/update.phtml b/airtime_mvc/application/views/scripts/library/update.phtml deleted file mode 100644 index 52eb9608b..000000000 --- a/airtime_mvc/application/views/scripts/library/update.phtml +++ /dev/null @@ -1,3 +0,0 @@ -partialLoop('library/libraryTablePartial.phtml', $this->files); From 99b490129c827a34cb8a5077122f3a82c8be2cf6 Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Thu, 1 Mar 2012 00:19:59 +0100 Subject: [PATCH 27/39] CC-3174 : showbuilder making a dialog on the calendar page. --- .../controllers/LibraryController.php | 1 + .../controllers/ScheduleController.php | 41 +- .../controllers/ShowbuilderController.php | 30 + airtime_mvc/application/models/Show.php | 25 +- .../scripts/showbuilder/builderDialog.phtml | 9 + .../public/css/datatables/css/ColVis.css | 6 +- airtime_mvc/public/css/showbuilder.css | 9 + airtime_mvc/public/css/styles.css | 4 +- .../public/js/airtime/library/library.js | 808 +++++++-------- .../public/js/airtime/library/main_library.js | 1 + .../public/js/airtime/schedule/schedule.js | 77 +- .../public/js/airtime/showbuilder/builder.js | 916 ++++++++---------- .../js/airtime/showbuilder/main_builder.js | 128 +++ 13 files changed, 1087 insertions(+), 968 deletions(-) create mode 100644 airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml create mode 100644 airtime_mvc/public/js/airtime/library/main_library.js create mode 100644 airtime_mvc/public/js/airtime/showbuilder/main_builder.js diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 772b83a4d..64d411086 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -57,6 +57,7 @@ class LibraryController extends Zend_Controller_Action $this->view->headScript()->appendFile($baseUrl.'/js/airtime/buttons/buttons.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/library.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/main_library.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); $this->view->headLink()->appendStylesheet($baseUrl.'/css/media_library.css?'.$CC_CONFIG['airtime_version']); $this->view->headLink()->appendStylesheet($baseUrl.'/css/jquery.contextMenu.css?'.$CC_CONFIG['airtime_version']); diff --git a/airtime_mvc/application/controllers/ScheduleController.php b/airtime_mvc/application/controllers/ScheduleController.php index 86678e8ba..a97471759 100644 --- a/airtime_mvc/application/controllers/ScheduleController.php +++ b/airtime_mvc/application/controllers/ScheduleController.php @@ -59,6 +59,29 @@ class ScheduleController extends Zend_Controller_Action $this->view->headLink()->appendStylesheet($baseUrl.'/css/add-show.css?'.$CC_CONFIG['airtime_version']); $this->view->headLink()->appendStylesheet($baseUrl.'/css/jquery.contextMenu.css?'.$CC_CONFIG['airtime_version']); + //Start Show builder JS/CSS requirements + $this->view->headScript()->appendFile($baseUrl.'/js/contextmenu/jquery.contextMenu.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/js/jquery.dataTables.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.pluginAPI.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.fnSetFilteringDelay.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.ColVis.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.ColReorder.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.FixedColumns.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/datatables/plugin/dataTables.TableTools.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + + $this->view->headScript()->appendFile($baseUrl.'/js/airtime/buttons/buttons.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($this->view->baseUrl('/js/airtime/library/events/library_showbuilder.js?'.$CC_CONFIG['airtime_version']),'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/airtime/library/library.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/airtime/showbuilder/builder.js?'.$CC_CONFIG['airtime_version'],'text/javascript'); + + $this->view->headLink()->appendStylesheet($baseUrl.'/css/media_library.css?'.$CC_CONFIG['airtime_version']); + $this->view->headLink()->appendStylesheet($baseUrl.'/css/jquery.contextMenu.css?'.$CC_CONFIG['airtime_version']); + $this->view->headLink()->appendStylesheet($baseUrl.'/css/datatables/css/ColVis.css?'.$CC_CONFIG['airtime_version']); + $this->view->headLink()->appendStylesheet($baseUrl.'/css/datatables/css/ColReorder.css?'.$CC_CONFIG['airtime_version']); + $this->view->headLink()->appendStylesheet($baseUrl.'/css/TableTools.css?'.$CC_CONFIG['airtime_version']); + $this->view->headLink()->appendStylesheet($baseUrl.'/css/showbuilder.css?'.$CC_CONFIG['airtime_version']); + //End Show builder JS/CSS requirements + Application_Model_Schedule::createNewFormSections($this->view); $userInfo = Zend_Auth::getInstance()->getStorage()->read(); @@ -78,10 +101,12 @@ class ScheduleController extends Zend_Controller_Action $userInfo = Zend_Auth::getInstance()->getStorage()->read(); $user = new Application_Model_User($userInfo->id); - if($user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER))) + if ($user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER))) { $editable = true; - else + } + else { $editable = false; + } $this->view->events = Application_Model_Show::getFullCalendarEvents($start, $end, $editable); } @@ -95,19 +120,19 @@ class ScheduleController extends Zend_Controller_Action $userInfo = Zend_Auth::getInstance()->getStorage()->read(); $user = new Application_Model_User($userInfo->id); - if($user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER))) { - try{ + if ($user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER))) { + try { $showInstance = new Application_Model_ShowInstance($showInstanceId); - }catch(Exception $e){ + } catch (Exception $e){ $this->view->show_error = true; return false; } $error = $showInstance->moveShow($deltaDay, $deltaMin); } - if(isset($error)) + if (isset($error)) { $this->view->error = $error; - + } } public function resizeShowAction() @@ -200,7 +225,7 @@ class ScheduleController extends Zend_Controller_Action && !$instance->isRebroadcast()) { $menu["schedule"] = array("name"=> "Add / Remove Content", - "url" => "/showbuilder/index/"); + "url" => "/showbuilder/builder-dialog/"); $menu["clear"] = array("name"=> "Remove All Content", "icon" => "delete", "url" => "/schedule/clear-show"); diff --git a/airtime_mvc/application/controllers/ShowbuilderController.php b/airtime_mvc/application/controllers/ShowbuilderController.php index ef8177f7c..1a9082988 100644 --- a/airtime_mvc/application/controllers/ShowbuilderController.php +++ b/airtime_mvc/application/controllers/ShowbuilderController.php @@ -9,6 +9,7 @@ class ShowbuilderController extends Zend_Controller_Action $ajaxContext->addActionContext('schedule-move', 'json') ->addActionContext('schedule-add', 'json') ->addActionContext('schedule-remove', 'json') + ->addActionContext('builder-dialog', 'json') ->addActionContext('builder-feed', 'json') ->initContext(); } @@ -53,11 +54,40 @@ class ShowbuilderController extends Zend_Controller_Action $this->view->headScript()->appendScript("var serverTimezoneOffset = {$offset}; //in seconds"); $this->view->headScript()->appendFile($baseUrl.'/js/timepicker/jquery.ui.timepicker.js','text/javascript'); $this->view->headScript()->appendFile($baseUrl.'/js/airtime/showbuilder/builder.js','text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'/js/airtime/showbuilder/main_builder.js','text/javascript'); $this->view->headLink()->appendStylesheet($baseUrl.'/css/jquery.ui.timepicker.css'); $this->view->headLink()->appendStylesheet($baseUrl.'/css/showbuilder.css'); } + public function builderDialogAction() { + + $request = $this->getRequest(); + $id = $request->getParam("id"); + + $instance = CcShowInstancesQuery::create()->findPK($id); + + if (is_null($instance)) { + $this->view->error = "show does not exist"; + return; + } + + $start = $instance->getDbStarts(null); + $start->setTimezone(new DateTimeZone(date_default_timezone_get())); + $end = $instance->getDbEnds(null); + $end->setTimezone(new DateTimeZone(date_default_timezone_get())); + + $show_name = $instance->getCcShow()->getDbName(); + $start_time = $start->format("Y-m-d H:i:s"); + $end_time = $end->format("Y-m-d H:i:s"); + + $this->view->title = "{$show_name}: {$start_time} - {$end_time}"; + $this->view->start = $instance->getDbStarts("U"); + $this->view->end = $instance->getDbEnds("U"); + + $this->view->dialog = $this->view->render('showbuilder/builderDialog.phtml'); + } + public function builderFeedAction() { $request = $this->getRequest(); diff --git a/airtime_mvc/application/models/Show.php b/airtime_mvc/application/models/Show.php index ec4cdbcfe..d9d91cf52 100644 --- a/airtime_mvc/application/models/Show.php +++ b/airtime_mvc/application/models/Show.php @@ -1491,7 +1491,7 @@ class Application_Model_Show { $events = array(); $interval = $start->diff($end); - $days = $interval->format('%a'); + $days = $interval->format('%a'); $shows = Application_Model_Show::getShows($start, $end); @@ -1508,10 +1508,9 @@ class Application_Model_Show { if ($editable && (strtotime($today_timestamp) < strtotime($show["starts"]))) { $options["editable"] = true; - $events[] = Application_Model_Show::makeFullCalendarEvent($show, $options); - } else { - $events[] = Application_Model_Show::makeFullCalendarEvent($show, $options); } + + $events[] = Application_Model_Show::makeFullCalendarEvent($show, $options); } return $events; @@ -1521,10 +1520,6 @@ class Application_Model_Show { { $event = array(); - if($show["rebroadcast"]) { - $event["disableResizing"] = true; - } - $startDateTime = new DateTime($show["starts"], new DateTimeZone("UTC")); $startDateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); @@ -1538,29 +1533,27 @@ class Application_Model_Show { $event["end"] = $endDateTime->format("Y-m-d H:i:s"); $event["endUnix"] = $endDateTime->format("U"); $event["allDay"] = false; - //$event["description"] = $show["description"]; $event["showId"] = intval($show["show_id"]); $event["record"] = intval($show["record"]); $event["rebroadcast"] = intval($show["rebroadcast"]); // get soundcloud_id - if(!is_null($show["file_id"])){ + if (!is_null($show["file_id"])){ $file = Application_Model_StoredFile::Recall($show["file_id"]); $soundcloud_id = $file->getSoundCloudId(); - }else{ - $soundcloud_id = null; } - $event["soundcloud_id"] = (is_null($soundcloud_id) ? -1 : $soundcloud_id); + + $event["soundcloud_id"] = isset($soundcloud_id) ? $soundcloud_id : -1; //event colouring - if($show["color"] != "") { + if ($show["color"] != "") { $event["textColor"] = "#".$show["color"]; } - if($show["background_color"] != "") { + if ($show["background_color"] != "") { $event["color"] = "#".$show["background_color"]; } - foreach($options as $key=>$value) { + foreach ($options as $key => $value) { $event[$key] = $value; } diff --git a/airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml b/airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml new file mode 100644 index 000000000..8c8bee2b5 --- /dev/null +++ b/airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml @@ -0,0 +1,9 @@ +
    +
    + +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/airtime_mvc/public/css/datatables/css/ColVis.css b/airtime_mvc/public/css/datatables/css/ColVis.css index 95987b876..f4be403c1 100644 --- a/airtime_mvc/public/css/datatables/css/ColVis.css +++ b/airtime_mvc/public/css/datatables/css/ColVis.css @@ -30,7 +30,7 @@ button.ColVis_Button::-moz-focus-inner { div.ColVis_collectionBackground { background-color: black; - z-index: 996; + z-index: 1003; } div.ColVis_collection { @@ -39,7 +39,7 @@ div.ColVis_collection { background-color: #999; padding: 3px; border: 1px solid #ccc; - z-index: 998; + z-index: 1005; } div.ColVis_collection button.ColVis_Button { @@ -51,7 +51,7 @@ div.ColVis_collection button.ColVis_Button { div.ColVis_catcher { position: absolute; - z-index: 997; + z-index: 1004; } .disabled { diff --git a/airtime_mvc/public/css/showbuilder.css b/airtime_mvc/public/css/showbuilder.css index 9854c67b4..eefd5bf57 100644 --- a/airtime_mvc/public/css/showbuilder.css +++ b/airtime_mvc/public/css/showbuilder.css @@ -34,4 +34,13 @@ tr.cursor-selected-row .marker { .sb-over { background-color:#ff3030; +} + +.ui-dialog .wrapper { + margin: 0; + padding: 10px 0 0 0; +} + +.ui-dialog .ui-buttonset { + margin-right: 0 !important; } \ No newline at end of file diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css index 9845cd260..5215c675e 100644 --- a/airtime_mvc/public/css/styles.css +++ b/airtime_mvc/public/css/styles.css @@ -601,6 +601,7 @@ dl.inline-list dd { } .dataTables_info { + float: left; padding: 8px 0 0 8px; font-size:12px; color:#555555; @@ -608,6 +609,7 @@ dl.inline-list dd { } .dataTables_paginate { + float: right; padding: 8px 0 8px 8px; } .dataTables_paginate .ui-button { @@ -618,7 +620,7 @@ dl.inline-list dd { } .dataTables_filter input { background: url("images/search_auto_bg.png") no-repeat scroll 0 0 #DDDDDD; - width: 60%; + width: 55%; border: 1px solid #5B5B5B; margin-left: -8px; padding: 4px 3px 4px 25px; diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 97ceea6b1..42aa267ef 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -1,5 +1,6 @@ var AIRTIME = (function(AIRTIME){ - var mod; + var mod, + libraryInit; if (AIRTIME.library === undefined) { AIRTIME.library = {}; @@ -40,6 +41,408 @@ var AIRTIME = (function(AIRTIME){ AIRTIME.library.fnDeleteItems(aMedia); }; + libraryInit = function() { + var oTable; + + oTable = $('#library_display').dataTable( { + + "aoColumns": [ + /* Checkbox */ {"sTitle": "", "mDataProp": "checkbox", "bSortable": false, "bSearchable": false, "sWidth": "25px", "sClass": "library_checkbox"}, + /* Type */ {"sTitle": "", "mDataProp": "image", "bSearchable": false, "sWidth": "25px", "sClass": "library_type", "iDataSort": 2}, + /* ftype */ {"sTitle": "", "mDataProp": "ftype", "bSearchable": false, "bVisible": false}, + /* Title */ {"sTitle": "Title", "mDataProp": "track_title", "sClass": "library_title"}, + /* Creator */ {"sTitle": "Creator", "mDataProp": "artist_name", "sClass": "library_creator"}, + /* Album */ {"sTitle": "Album", "mDataProp": "album_title", "sClass": "library_album"}, + /* Genre */ {"sTitle": "Genre", "mDataProp": "genre", "sClass": "library_genre"}, + /* Year */ {"sTitle": "Year", "mDataProp": "year", "sClass": "library_year", "sWidth": "60px"}, + /* Length */ {"sTitle": "Length", "mDataProp": "length", "sClass": "library_length", "sWidth": "80px"}, + /* Upload Time */ {"sTitle": "Uploaded", "mDataProp": "utime", "sClass": "library_upload_time"}, + /* Last Modified */ {"sTitle": "Last Modified", "mDataProp": "mtime", "bVisible": false, "sClass": "library_modified_time"}, + /* Track Number */ {"sTitle": "Track", "mDataProp": "track_number", "bSearchable": false, "bVisible": false, "sClass": "library_track"}, + /* Mood */ {"sTitle": "Mood", "mDataProp": "mood", "bSearchable": false, "bVisible": false, "sClass": "library_mood"}, + /* BPM */ {"sTitle": "BPM", "mDataProp": "bpm", "bSearchable": false, "bVisible": false, "sClass": "library_bpm"}, + /* Composer */ {"sTitle": "Composer", "mDataProp": "composer", "bSearchable": false, "bVisible": false, "sClass": "library_composer"}, + /* Website */ {"sTitle": "Website", "mDataProp": "info_url", "bSearchable": false, "bVisible": false, "sClass": "library_url"}, + /* Bit Rate */ {"sTitle": "Bit Rate", "mDataProp": "bit_rate", "bSearchable": false, "bVisible": false, "sClass": "library_bitrate", "sWidth": "80px"}, + /* Sample Rate */ {"sTitle": "Sample", "mDataProp": "sample_rate", "bSearchable": false, "bVisible": false, "sClass": "library_sr", "sWidth": "80px"}, + /* ISRC Number */ {"sTitle": "ISRC", "mDataProp": "isrc_number", "bSearchable": false, "bVisible": false, "sClass": "library_isrc"}, + /* Encoded */ {"sTitle": "Encoded", "mDataProp": "encoded_by", "bSearchable": false, "bVisible": false, "sClass": "library_encoded"}, + /* Label */ {"sTitle": "Label", "mDataProp": "label", "bSearchable": false, "bVisible": false, "sClass": "library_label"}, + /* Copyright */ {"sTitle": "Copyright", "mDataProp": "copyright", "bSearchable": false, "bVisible": false, "sClass": "library_copyright"}, + /* Mime */ {"sTitle": "Mime", "mDataProp": "mime", "bSearchable": false, "bVisible": false, "sClass": "library_mime"}, + /* Language */ {"sTitle": "Language", "mDataProp": "language", "bSearchable": false, "bVisible": false, "sClass": "library_language"} + ], + + "bProcessing": true, + "bServerSide": true, + + "bStateSave": true, + "fnStateSaveParams": function (oSettings, oData) { + //remove oData components we don't want to save. + delete oData.oSearch; + delete oData.aoSearchCols; + }, + "fnStateSave": function (oSettings, oData) { + + $.ajax({ + url: "/usersettings/set-library-datatable", + type: "POST", + data: {settings : oData, format: "json"}, + dataType: "json", + success: function(){}, + error: function (jqXHR, textStatus, errorThrown) { + var x; + } + }); + }, + "fnStateLoad": function (oSettings) { + var o; + + $.ajax({ + url: "/usersettings/get-library-datatable", + type: "GET", + data: {format: "json"}, + dataType: "json", + async: false, + success: function(json){ + o = json.settings; + }, + error: function (jqXHR, textStatus, errorThrown) { + var x; + } + }); + + return o; + }, + "fnStateLoadParams": function (oSettings, oData) { + var i, + length, + a = oData.abVisCols; + + //putting serialized data back into the correct js type to make + //sure everything works properly. + for (i = 0, length = a.length; i < length; i++) { + a[i] = (a[i] === "true") ? true : false; + } + + a = oData.ColReorder; + for (i = 0, length = a.length; i < length; i++) { + a[i] = parseInt(a[i], 10); + } + + oData.iEnd = parseInt(oData.iEnd, 10); + oData.iLength = parseInt(oData.iLength, 10); + oData.iStart = parseInt(oData.iStart, 10); + oData.iCreate = parseInt(oData.iCreate, 10); + }, + + "sAjaxSource": "/Library/contents", + "fnServerData": function ( sSource, aoData, fnCallback ) { + var type; + + aoData.push( { name: "format", value: "json"} ); + + //push whether to search files/playlists or all. + type = $("#library_display_type").find("select").val(); + type = (type === undefined) ? 0 : type; + aoData.push( { name: "type", value: type} ); + + $.ajax( { + "dataType": 'json', + "type": "GET", + "url": sSource, + "data": aoData, + "success": fnCallback + } ); + }, + "fnRowCallback": AIRTIME.library.events.fnRowCallback, + "fnCreatedRow": function( nRow, aData, iDataIndex ) { + + //call the context menu so we can prevent the event from propagating. + $(nRow).find('td:not(.library_checkbox)').click(function(e){ + + $(this).contextMenu({x: e.pageX, y: e.pageY}); + + return false; + }); + + //add a tool tip to appear when the user clicks on the type icon. + $(nRow).find("td:not(:first, td>img)").qtip({ + content: { + text: "Loading...", + title: { + text: aData.track_title + }, + ajax: { + url: "/Library/get-file-meta-data", + type: "get", + data: ({format: "html", id : aData.id, type: aData.ftype}), + success: function(data, status) { + this.set('content.text', data); + } + } + }, + position: { + target: 'event', + adjust: { + resize: true, + method: "flip flip" + }, + my: 'left center', + at: 'right center', + viewport: $(window), // Keep the tooltip on-screen at all times + effect: false // Disable positioning animation + }, + style: { + classes: "ui-tooltip-dark" + }, + show: 'mousedown', + events: { + show: function(event, api) { + // Only show the tooltip if it was a right-click + if(event.originalEvent.button !== 2) { + event.preventDefault(); + } + } + }, + hide: 'mouseout' + + }); + }, + "fnDrawCallback": AIRTIME.library.events.fnDrawCallback, + "fnHeaderCallback": function(nHead) { + $(nHead).find("input[type=checkbox]").attr("checked", false); + }, + + "aaSorting": [[3, 'asc']], + "sPaginationType": "full_numbers", + "bJQueryUI": true, + "bAutoWidth": false, + "oLanguage": { + "sSearch": "" + }, + + // R = ColReorder, C = ColVis, T = TableTools + "sDom": 'Rl<"#library_display_type">fr<"H"T<"library_toolbar"C>>t<"F"ip>', + + "oTableTools": { + "sRowSelect": "multi", + "aButtons": [], + "fnRowSelected": function ( node ) { + var selected; + + //seems to happen if everything is selected + if ( node === null) { + selected = oTable.find("input[type=checkbox]"); + selected.attr("checked", true); + } + else { + $(node).find("input[type=checkbox]").attr("checked", true); + selected = oTable.find("input[type=checkbox]").filter(":checked"); + } + + //checking to enable buttons + AIRTIME.button.enableButton("library_group_delete"); + AIRTIME.library.events.enableAddButtonCheck(); + }, + "fnRowDeselected": function ( node ) { + var selected; + + //seems to happen if everything is deselected + if ( node === null) { + oTable.find("input[type=checkbox]").attr("checked", false); + selected = []; + } + else { + $(node).find("input[type=checkbox]").attr("checked", false); + selected = oTable.find("input[type=checkbox]").filter(":checked"); + } + + //checking to disable buttons + if (selected.length === 0) { + AIRTIME.button.disableButton("library_group_delete"); + } + AIRTIME.library.events.enableAddButtonCheck(); + } + }, + + "oColVis": { + "buttonText": "Show/Hide Columns", + "sAlign": "right", + "aiExclude": [0, 1, 2], + "sSize": "css" + }, + + "oColReorder": { + "iFixedColumns": 2 + } + + }); + oTable.fnSetFilteringDelay(350); + + AIRTIME.library.events.setupLibraryToolbar(oTable); + + $("#library_display_type") + .addClass("dataTables_type") + .append('", "mDataProp": "checkbox", "bSortable": false, "bSearchable": false, "sWidth": "25px", "sClass": "library_checkbox"}, - /* Type */ {"sTitle": "", "mDataProp": "image", "bSearchable": false, "sWidth": "25px", "sClass": "library_type", "iDataSort": 2}, - /* ftype */ {"sTitle": "", "mDataProp": "ftype", "bSearchable": false, "bVisible": false}, - /* Title */ {"sTitle": "Title", "mDataProp": "track_title", "sClass": "library_title"}, - /* Creator */ {"sTitle": "Creator", "mDataProp": "artist_name", "sClass": "library_creator"}, - /* Album */ {"sTitle": "Album", "mDataProp": "album_title", "sClass": "library_album"}, - /* Genre */ {"sTitle": "Genre", "mDataProp": "genre", "sClass": "library_genre"}, - /* Year */ {"sTitle": "Year", "mDataProp": "year", "sClass": "library_year", "sWidth": "60px"}, - /* Length */ {"sTitle": "Length", "mDataProp": "length", "sClass": "library_length", "sWidth": "80px"}, - /* Upload Time */ {"sTitle": "Uploaded", "mDataProp": "utime", "sClass": "library_upload_time"}, - /* Last Modified */ {"sTitle": "Last Modified", "mDataProp": "mtime", "bVisible": false, "sClass": "library_modified_time"}, - /* Track Number */ {"sTitle": "Track", "mDataProp": "track_number", "bSearchable": false, "bVisible": false, "sClass": "library_track"}, - /* Mood */ {"sTitle": "Mood", "mDataProp": "mood", "bSearchable": false, "bVisible": false, "sClass": "library_mood"}, - /* BPM */ {"sTitle": "BPM", "mDataProp": "bpm", "bSearchable": false, "bVisible": false, "sClass": "library_bpm"}, - /* Composer */ {"sTitle": "Composer", "mDataProp": "composer", "bSearchable": false, "bVisible": false, "sClass": "library_composer"}, - /* Website */ {"sTitle": "Website", "mDataProp": "info_url", "bSearchable": false, "bVisible": false, "sClass": "library_url"}, - /* Bit Rate */ {"sTitle": "Bit Rate", "mDataProp": "bit_rate", "bSearchable": false, "bVisible": false, "sClass": "library_bitrate", "sWidth": "80px"}, - /* Sample Rate */ {"sTitle": "Sample", "mDataProp": "sample_rate", "bSearchable": false, "bVisible": false, "sClass": "library_sr", "sWidth": "80px"}, - /* ISRC Number */ {"sTitle": "ISRC", "mDataProp": "isrc_number", "bSearchable": false, "bVisible": false, "sClass": "library_isrc"}, - /* Encoded */ {"sTitle": "Encoded", "mDataProp": "encoded_by", "bSearchable": false, "bVisible": false, "sClass": "library_encoded"}, - /* Label */ {"sTitle": "Label", "mDataProp": "label", "bSearchable": false, "bVisible": false, "sClass": "library_label"}, - /* Copyright */ {"sTitle": "Copyright", "mDataProp": "copyright", "bSearchable": false, "bVisible": false, "sClass": "library_copyright"}, - /* Mime */ {"sTitle": "Mime", "mDataProp": "mime", "bSearchable": false, "bVisible": false, "sClass": "library_mime"}, - /* Language */ {"sTitle": "Language", "mDataProp": "language", "bSearchable": false, "bVisible": false, "sClass": "library_language"} - ], - - "bProcessing": true, - "bServerSide": true, - - "bStateSave": true, - "fnStateSaveParams": function (oSettings, oData) { - //remove oData components we don't want to save. - delete oData.oSearch; - delete oData.aoSearchCols; - }, - "fnStateSave": function (oSettings, oData) { - - $.ajax({ - url: "/usersettings/set-library-datatable", - type: "POST", - data: {settings : oData, format: "json"}, - dataType: "json", - success: function(){}, - error: function (jqXHR, textStatus, errorThrown) { - var x; - } - }); - }, - "fnStateLoad": function (oSettings) { - var o; - - $.ajax({ - url: "/usersettings/get-library-datatable", - type: "GET", - data: {format: "json"}, - dataType: "json", - async: false, - success: function(json){ - o = json.settings; - }, - error: function (jqXHR, textStatus, errorThrown) { - var x; - } - }); - - return o; - }, - "fnStateLoadParams": function (oSettings, oData) { - var i, - length, - a = oData.abVisCols; - - //putting serialized data back into the correct js type to make - //sure everything works properly. - for (i = 0, length = a.length; i < length; i++) { - a[i] = (a[i] === "true") ? true : false; - } - - a = oData.ColReorder; - for (i = 0, length = a.length; i < length; i++) { - a[i] = parseInt(a[i], 10); - } - - oData.iEnd = parseInt(oData.iEnd, 10); - oData.iLength = parseInt(oData.iLength, 10); - oData.iStart = parseInt(oData.iStart, 10); - oData.iCreate = parseInt(oData.iCreate, 10); - }, - - "sAjaxSource": "/Library/contents", - "fnServerData": function ( sSource, aoData, fnCallback ) { - var type; - - aoData.push( { name: "format", value: "json"} ); - - //push whether to search files/playlists or all. - type = $("#library_display_type").find("select").val(); - type = (type === undefined) ? 0 : type; - aoData.push( { name: "type", value: type} ); - - $.ajax( { - "dataType": 'json', - "type": "GET", - "url": sSource, - "data": aoData, - "success": fnCallback - } ); - }, - "fnRowCallback": AIRTIME.library.events.fnRowCallback, - "fnCreatedRow": function( nRow, aData, iDataIndex ) { - - //call the context menu so we can prevent the event from propagating. - $(nRow).find('td:not(.library_checkbox)').click(function(e){ - - $(this).contextMenu({x: e.pageX, y: e.pageY}); - - return false; - }); - - //add a tool tip to appear when the user clicks on the type icon. - $(nRow).find("td:not(:first, td>img)").qtip({ - content: { - text: "Loading...", - title: { - text: aData.track_title - }, - ajax: { - url: "/Library/get-file-meta-data", - type: "get", - data: ({format: "html", id : aData.id, type: aData.ftype}), - success: function(data, status) { - this.set('content.text', data); - } - } - }, - position: { - target: 'event', - adjust: { - resize: true, - method: "flip flip" - }, - my: 'left center', - at: 'right center', - viewport: $(window), // Keep the tooltip on-screen at all times - effect: false // Disable positioning animation - }, - style: { - classes: "ui-tooltip-dark" - }, - show: 'mousedown', - events: { - show: function(event, api) { - // Only show the tooltip if it was a right-click - if(event.originalEvent.button !== 2) { - event.preventDefault(); - } - } - }, - hide: 'mouseout' - - }); - }, - "fnDrawCallback": AIRTIME.library.events.fnDrawCallback, - "fnHeaderCallback": function(nHead) { - $(nHead).find("input[type=checkbox]").attr("checked", false); - }, - - "aaSorting": [[3, 'asc']], - "sPaginationType": "full_numbers", - "bJQueryUI": true, - "bAutoWidth": false, - "oLanguage": { - "sSearch": "" - }, - - // R = ColReorder, C = ColVis, T = TableTools - "sDom": 'Rl<"#library_display_type">fr<"H"T<"library_toolbar"C>>t<"F"ip>', - - "oTableTools": { - "sRowSelect": "multi", - "aButtons": [], - "fnRowSelected": function ( node ) { - var selected; - - //seems to happen if everything is selected - if ( node === null) { - selected = oTable.find("input[type=checkbox]"); - selected.attr("checked", true); - } - else { - $(node).find("input[type=checkbox]").attr("checked", true); - selected = oTable.find("input[type=checkbox]").filter(":checked"); - } - - //checking to enable buttons - AIRTIME.button.enableButton("library_group_delete"); - AIRTIME.library.events.enableAddButtonCheck(); - }, - "fnRowDeselected": function ( node ) { - var selected; - - //seems to happen if everything is deselected - if ( node === null) { - oTable.find("input[type=checkbox]").attr("checked", false); - selected = []; - } - else { - $(node).find("input[type=checkbox]").attr("checked", false); - selected = oTable.find("input[type=checkbox]").filter(":checked"); - } - - //checking to disable buttons - if (selected.length === 0) { - AIRTIME.button.disableButton("library_group_delete"); - } - AIRTIME.library.events.enableAddButtonCheck(); - } - }, - - "oColVis": { - "buttonText": "Show/Hide Columns", - "sAlign": "right", - "aiExclude": [0, 1, 2], - "sSize": "css" - }, - - "oColReorder": { - "iFixedColumns": 2 - } - - }); - oTable.fnSetFilteringDelay(350); - - AIRTIME.library.events.setupLibraryToolbar(oTable); - - $("#library_display_type") - .addClass("dataTables_type") - .append('", "sWidth": "15px"}, - /* starts */{"mDataProp": "starts", "sTitle": "Start"}, - /* ends */{"mDataProp": "ends", "sTitle": "End"}, - /* runtime */{"mDataProp": "runtime", "sTitle": "Duration", "sClass": "library_length"}, - /* title */{"mDataProp": "title", "sTitle": "Title"}, - /* creator */{"mDataProp": "creator", "sTitle": "Creator"}, - /* album */{"mDataProp": "album", "sTitle": "Album"}, - /* cue in */{"mDataProp": "cuein", "sTitle": "Cue In", "bVisible": false}, - /* cue out */{"mDataProp": "cueout", "sTitle": "Cue Out", "bVisible": false}, - /* fade in */{"mDataProp": "fadein", "sTitle": "Fade In", "bVisible": false}, - /* fade out */{"mDataProp": "fadeout", "sTitle": "Fade Out", "bVisible": false} - ], - - "bJQueryUI": true, - "bSort": false, - "bFilter": false, - "bProcessing": true, - "bServerSide": true, - "bInfo": false, - "bAutoWidth": false, - - "bStateSave": true, - "fnStateSaveParams": function (oSettings, oData) { - //remove oData components we don't want to save. - delete oData.oSearch; - delete oData.aoSearchCols; - }, - "fnStateSave": function (oSettings, oData) { - - $.ajax({ - url: "/usersettings/set-timeline-datatable", - type: "POST", - data: {settings : oData, format: "json"}, - dataType: "json", - success: function(){}, - error: function (jqXHR, textStatus, errorThrown) { - var x; - } - }); - }, - "fnStateLoad": function (oSettings) { - var o; + mod.builderDataTable = function() { + var tableDiv = $('#show_builder_table'), + oTable, + fnRemoveSelectedItems; - $.ajax({ - url: "/usersettings/get-timeline-datatable", - type: "GET", - data: {format: "json"}, - dataType: "json", - async: false, - success: function(json){ - o = json.settings; - }, - error: function (jqXHR, textStatus, errorThrown) { - var x; - } - }); - - return o; - }, - "fnStateLoadParams": function (oSettings, oData) { - var i, + fnRemoveSelectedItems = function() { + var oTT = TableTools.fnGetInstance('show_builder_table'), + aData = oTT.fnGetSelectedData(), + i, length, - a = oData.abVisCols; + temp, + aItems = []; - //putting serialized data back into the correct js type to make - //sure everything works properly. - for (i = 0, length = a.length; i < length; i++) { - a[i] = (a[i] === "true") ? true : false; - } + for (i=0, length = aData.length; i < length; i++) { + temp = aData[i]; + aItems.push({"id": temp.id, "instance": temp.instance, "timestamp": temp.timestamp}); + } + + AIRTIME.showbuilder.fnRemove(aItems); + }; + + oTable = tableDiv.dataTable( { + "aoColumns": [ + /* checkbox */ {"mDataProp": "allowed", "sTitle": "", "sWidth": "15px"}, + /* starts */{"mDataProp": "starts", "sTitle": "Start"}, + /* ends */{"mDataProp": "ends", "sTitle": "End"}, + /* runtime */{"mDataProp": "runtime", "sTitle": "Duration", "sClass": "library_length"}, + /* title */{"mDataProp": "title", "sTitle": "Title"}, + /* creator */{"mDataProp": "creator", "sTitle": "Creator"}, + /* album */{"mDataProp": "album", "sTitle": "Album"}, + /* cue in */{"mDataProp": "cuein", "sTitle": "Cue In", "bVisible": false}, + /* cue out */{"mDataProp": "cueout", "sTitle": "Cue Out", "bVisible": false}, + /* fade in */{"mDataProp": "fadein", "sTitle": "Fade In", "bVisible": false}, + /* fade out */{"mDataProp": "fadeout", "sTitle": "Fade Out", "bVisible": false} + ], - a = oData.ColReorder; - for (i = 0, length = a.length; i < length; i++) { - a[i] = parseInt(a[i], 10); - } - - oData.iCreate = parseInt(oData.iCreate, 10); - }, - - "fnServerData": fnServerData, - "fnRowCallback": function ( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { - var i, - sSeparatorHTML, - fnPrepareSeparatorRow, - node, - cl=""; + "bJQueryUI": true, + "bSort": false, + "bFilter": false, + "bProcessing": true, + "bServerSide": true, + "bInfo": false, + "bAutoWidth": false, - //save some info for reordering purposes. - $(nRow).data({"aData": aData}); + "bStateSave": true, + "fnStateSaveParams": function (oSettings, oData) { + //remove oData components we don't want to save. + delete oData.oSearch; + delete oData.aoSearchCols; + }, + "fnStateSave": function (oSettings, oData) { + + $.ajax({ + url: "/usersettings/set-timeline-datatable", + type: "POST", + data: {settings : oData, format: "json"}, + dataType: "json", + success: function(){}, + error: function (jqXHR, textStatus, errorThrown) { + var x; + } + }); + }, + "fnStateLoad": function (oSettings) { + var o; + + $.ajax({ + url: "/usersettings/get-timeline-datatable", + type: "GET", + data: {format: "json"}, + dataType: "json", + async: false, + success: function(json){ + o = json.settings; + }, + error: function (jqXHR, textStatus, errorThrown) { + var x; + } + }); + + return o; + }, + "fnStateLoadParams": function (oSettings, oData) { + var i, + length, + a = oData.abVisCols; - if (aData.allowed !== true) { - $(nRow).addClass("sb-not-allowed"); - } - else { - $(nRow).addClass("sb-allowed"); - } - - //status used to colour tracks. - if (aData.status === 1) { - $(nRow).addClass("sb-boundry"); - } - else if (aData.status === 2) { - $(nRow).addClass("sb-over"); - } - - fnPrepareSeparatorRow = function(sRowContent, sClass, iNodeIndex) { + //putting serialized data back into the correct js type to make + //sure everything works properly. + for (i = 0, length = a.length; i < length; i++) { + a[i] = (a[i] === "true") ? true : false; + } + + a = oData.ColReorder; + for (i = 0, length = a.length; i < length; i++) { + a[i] = parseInt(a[i], 10); + } + + oData.iCreate = parseInt(oData.iCreate, 10); + }, + + "fnServerData": AIRTIME.showbuilder.fnServerData, + "fnRowCallback": function ( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { + var i, + sSeparatorHTML, + fnPrepareSeparatorRow, + node, + cl=""; - node = nRow.children[iNodeIndex]; - node.innerHTML = sRowContent; - node.setAttribute('colspan',100); - for (i = iNodeIndex + 1; i < nRow.children.length; i = i+1) { - node = nRow.children[i]; - node.innerHTML = ""; - node.setAttribute("style", "display : none"); + //save some info for reordering purposes. + $(nRow).data({"aData": aData}); + + if (aData.allowed !== true) { + $(nRow).addClass("sb-not-allowed"); + } + else { + $(nRow).addClass("sb-allowed"); } - $(nRow).addClass(sClass); + //status used to colour tracks. + if (aData.status === 1) { + $(nRow).addClass("sb-boundry"); + } + else if (aData.status === 2) { + $(nRow).addClass("sb-over"); + } + + fnPrepareSeparatorRow = function(sRowContent, sClass, iNodeIndex) { + + node = nRow.children[iNodeIndex]; + node.innerHTML = sRowContent; + node.setAttribute('colspan',100); + for (i = iNodeIndex + 1; i < nRow.children.length; i = i+1) { + node = nRow.children[i]; + node.innerHTML = ""; + node.setAttribute("style", "display : none"); + } + + $(nRow).addClass(sClass); + }; + + if (aData.header === true) { + cl = 'sb-header'; + + sSeparatorHTML = ''+aData.title+''+aData.starts+''+aData.ends+''; + fnPrepareSeparatorRow(sSeparatorHTML, cl, 0); + } + else if (aData.footer === true) { + node = nRow.children[0]; + cl = 'sb-footer'; + + //check the show's content status. + if (aData.runtime > 0) { + node.innerHTML = ''; + cl = cl + ' ui-state-highlight'; + } + else { + node.innerHTML = ''; + cl = cl + ' ui-state-error'; + } + + sSeparatorHTML = ''+aData.fRuntime+''; + fnPrepareSeparatorRow(sSeparatorHTML, cl, 1); + } + else if (aData.empty === true) { + + sSeparatorHTML = 'Show Empty'; + cl = cl + " sb-empty odd"; + + fnPrepareSeparatorRow(sSeparatorHTML, cl, 0); + } + else { + + node = nRow.children[0]; + if (aData.allowed === true) { + node.innerHTML = ''; + } + else { + node.innerHTML = ''; + } + } + }, + "fnDrawCallback": function(oSettings, json) { + var wrapperDiv, + markerDiv, + td; + + //create cursor arrows. + tableDiv.find("tr:not(:first, .sb-footer, .sb-empty, .sb-not-allowed)").each(function(i, el) { + td = $(el).find("td:first"); + if (td.hasClass("dataTables_empty")) { + return false; + } + + wrapperDiv = $("
    ", { + "class": "innerWrapper", + "css": { + "height": td.height() + } + }); + markerDiv = $("
    ", { + "class": "marker" + }); + + td.append(markerDiv).wrapInner(wrapperDiv); + }); + }, + "fnHeaderCallback": function(nHead) { + $(nHead).find("input[type=checkbox]").attr("checked", false); + }, + //remove any selected nodes before the draw. + "fnPreDrawCallback": function( oSettings ) { + var oTT = TableTools.fnGetInstance('show_builder_table'); + oTT.fnSelectNone(); + }, + + "oColVis": { + "aiExclude": [ 0, 1 ] + }, + + "oColReorder": { + "iFixedColumns": 2 + }, + + "oTableTools": { + "sRowSelect": "multi", + "aButtons": [], + "fnPreRowSelect": function ( e ) { + var node = e.currentTarget; + //don't select separating rows, or shows without privileges. + if ($(node).hasClass("sb-header") + || $(node).hasClass("sb-footer") + || $(node).hasClass("sb-empty") + || $(node).hasClass("sb-not-allowed")) { + return false; + } + return true; + }, + "fnRowSelected": function ( node ) { + + //seems to happen if everything is selected + if ( node === null) { + oTable.find("input[type=checkbox]").attr("checked", true); + } + else { + $(node).find("input[type=checkbox]").attr("checked", true); + } + + //checking to enable buttons + AIRTIME.button.enableButton("sb_delete"); + }, + "fnRowDeselected": function ( node ) { + var selected; + + //seems to happen if everything is deselected + if ( node === null) { + tableDiv.find("input[type=checkbox]").attr("checked", false); + selected = []; + } + else { + $(node).find("input[type=checkbox]").attr("checked", false); + selected = tableDiv.find("input[type=checkbox]").filter(":checked"); + } + + //checking to disable buttons + if (selected.length === 0) { + AIRTIME.button.disableButton("sb_delete"); + } + } + }, + + // R = ColReorderResize, C = ColVis, T = TableTools + "sDom": 'Rr<"H"CT>t<"F">', + + "sAjaxDataProp": "schedule", + "sAjaxSource": "/showbuilder/builder-feed" + }); + + $('[name="sb_cb_all"]').click(function(){ + var oTT = TableTools.fnGetInstance('show_builder_table'); + + if ($(this).is(":checked")) { + var allowedNodes; + + allowedNodes = oTable.find('tr:not(:first, .sb-header, .sb-empty, .sb-footer, .sb-not-allowed)'); + + allowedNodes.each(function(i, el){ + oTT.fnSelect(el); + }); + } + else { + oTT.fnSelectNone(); + } + }); + + var sortableConf = (function(){ + var origTrs, + aItemData = [], + oPrevData, + fnAdd, + fnMove, + fnReceive, + fnUpdate, + i, + html; + + fnAdd = function() { + var aMediaIds = [], + aSchedIds = []; + + for(i = 0; i < aItemData.length; i++) { + aMediaIds.push({"id": aItemData[i].id, "type": aItemData[i].ftype}); + } + aSchedIds.push({"id": oPrevData.id, "instance": oPrevData.instance, "timestamp": oPrevData.timestamp}); + + AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds); }; - if (aData.header === true) { - cl = 'sb-header'; + fnMove = function() { + var aSelect = [], + aAfter = []; + + aSelect.push({"id": aItemData[0].id, "instance": aItemData[0].instance, "timestamp": aItemData[0].timestamp}); + aAfter.push({"id": oPrevData.id, "instance": oPrevData.instance, "timestamp": oPrevData.timestamp}); + + AIRTIME.showbuilder.fnMove(aSelect, aAfter); + }; + + fnReceive = function(event, ui) { + var aItems = [], + oLibTT = TableTools.fnGetInstance('library_display'); + + aItems = oLibTT.fnGetSelectedData(); - sSeparatorHTML = ''+aData.title+''+aData.starts+''+aData.ends+''; - fnPrepareSeparatorRow(sSeparatorHTML, cl, 0); - } - else if (aData.footer === true) { - node = nRow.children[0]; - cl = 'sb-footer'; + //if nothing is checked select the dragged item. + if (aItems.length === 0) { + aItems.push(ui.item.data("aData")); + } + + origTrs = aItems; + html = ui.helper.html(); + }; + + fnUpdate = function(event, ui) { + var prev = ui.item.prev(); - //check the show's content status. - if (aData.runtime > 0) { - node.innerHTML = ''; - cl = cl + ' ui-state-highlight'; - } - else { - node.innerHTML = ''; - cl = cl + ' ui-state-error'; + //can't add items outside of shows. + if (!prev.hasClass("sb-allowed")) { + alert("Cannot schedule outside a show."); + ui.item.remove(); + return; } + + aItemData = []; + oPrevData = prev.data("aData"); + + //item was dragged in + if (origTrs !== undefined) { - sSeparatorHTML = ''+aData.fRuntime+''; - fnPrepareSeparatorRow(sSeparatorHTML, cl, 1); - } - else if (aData.empty === true) { - - sSeparatorHTML = 'Show Empty'; - cl = cl + " sb-empty odd"; - - fnPrepareSeparatorRow(sSeparatorHTML, cl, 0); - } - else { - - node = nRow.children[0]; - if (aData.allowed === true) { - node.innerHTML = ''; + $("#show_builder_table tr.ui-draggable") + .empty() + .after(html); + + aItemData = origTrs; + origTrs = undefined; + fnAdd(); } + //item was reordered. else { - node.innerHTML = ''; + aItemData.push(ui.item.data("aData")); + fnMove(); } + }; + + return { + placeholder: "placeholder show-builder-placeholder ui-state-highlight", + forcePlaceholderSize: true, + items: 'tr:not(:first, :last, .sb-header, .sb-footer, .sb-not-allowed)', + receive: fnReceive, + update: fnUpdate + }; + }()); + + tableDiv.sortable(sortableConf); + + $("#show_builder .fg-toolbar") + .append('
    ') + .click(fnRemoveSelectedItems); + + //set things like a reference to the table. + AIRTIME.showbuilder.init(oTable); + + //add event to cursors. + tableDiv.find("tbody").on("click", "div.marker", function(event){ + var tr = $(this).parents("tr"), + cursorSelClass = "cursor-selected-row"; + + if (tr.hasClass(cursorSelClass)) { + tr.removeClass(cursorSelClass); } - }, - "fnDrawCallback": function(oSettings, json) { - var wrapperDiv, - markerDiv, - td; - - //create cursor arrows. - tableDiv.find("tr:not(:first, .sb-footer, .sb-empty, .sb-not-allowed)").each(function(i, el) { - td = $(el).find("td:first"); - if (td.hasClass("dataTables_empty")) { - return false; - } - - wrapperDiv = $("
    ", { - "class": "innerWrapper", - "css": { - "height": td.height() - } - }); - markerDiv = $("
    ", { - "class": "marker" - }); - - td.append(markerDiv).wrapInner(wrapperDiv); - }); - }, - "fnHeaderCallback": function(nHead) { - $(nHead).find("input[type=checkbox]").attr("checked", false); - }, - //remove any selected nodes before the draw. - "fnPreDrawCallback": function( oSettings ) { - var oTT = TableTools.fnGetInstance('show_builder_table'); - oTT.fnSelectNone(); - }, - - "oColVis": { - "aiExclude": [ 0, 1 ] - }, - - "oColReorder": { - "iFixedColumns": 2 - }, - - "oTableTools": { - "sRowSelect": "multi", - "aButtons": [], - "fnPreRowSelect": function ( e ) { - var node = e.currentTarget; - //don't select separating rows, or shows without privileges. - if ($(node).hasClass("sb-header") - || $(node).hasClass("sb-footer") - || $(node).hasClass("sb-empty") - || $(node).hasClass("sb-not-allowed")) { - return false; - } - return true; - }, - "fnRowSelected": function ( node ) { - - //seems to happen if everything is selected - if ( node === null) { - oTable.find("input[type=checkbox]").attr("checked", true); - } - else { - $(node).find("input[type=checkbox]").attr("checked", true); - } - - //checking to enable buttons - AIRTIME.button.enableButton("sb_delete"); - }, - "fnRowDeselected": function ( node ) { - var selected; - - //seems to happen if everything is deselected - if ( node === null) { - tableDiv.find("input[type=checkbox]").attr("checked", false); - selected = []; - } - else { - $(node).find("input[type=checkbox]").attr("checked", false); - selected = tableDiv.find("input[type=checkbox]").filter(":checked"); - } - - //checking to disable buttons - if (selected.length === 0) { - AIRTIME.button.disableButton("sb_delete"); - } - } - }, - - // R = ColReorderResize, C = ColVis, T = TableTools - "sDom": 'Rr<"H"CT>t<"F">', - - "sAjaxDataProp": "schedule", - "sAjaxSource": "/showbuilder/builder-feed" - }); - - $('[name="sb_cb_all"]').click(function(){ - var oTT = TableTools.fnGetInstance('show_builder_table'); - - if ($(this).is(":checked")) { - var allowedNodes; - - allowedNodes = oTable.find('tr:not(:first, .sb-header, .sb-empty, .sb-footer, .sb-not-allowed)'); - - allowedNodes.each(function(i, el){ - oTT.fnSelect(el); - }); - } - else { - oTT.fnSelectNone(); - } - }); - - $("#sb_date_start").datepicker(oBaseDatePickerSettings); - $("#sb_time_start").timepicker(oBaseTimePickerSettings); - $("#sb_date_end").datepicker(oBaseDatePickerSettings); - $("#sb_time_end").timepicker(oBaseTimePickerSettings); - - $("#sb_submit").click(function(ev){ - var fn, - oRange, - op; - - oRange = fnGetScheduleRange(); - - fn = oTable.fnSettings().fnServerData; - fn.start = oRange.start; - fn.end = oRange.end; - - op = $("div.sb-advanced-options"); - if (op.is(":visible")) { - - if (fn.ops === undefined) { - fn.ops = {}; - } - fn.ops.showFilter = op.find("#sb_show_filter").val(); - fn.ops.myShows = op.find("#sb_my_shows").is(":checked") ? 1 : 0; - } - - oTable.fnDraw(); - }); - - var sortableConf = (function(){ - var origTrs, - aItemData = [], - oPrevData, - fnAdd, - fnMove, - fnReceive, - fnUpdate, - i, - html; - - fnAdd = function() { - var aMediaIds = [], - aSchedIds = []; - - for(i = 0; i < aItemData.length; i++) { - aMediaIds.push({"id": aItemData[i].id, "type": aItemData[i].ftype}); - } - aSchedIds.push({"id": oPrevData.id, "instance": oPrevData.instance, "timestamp": oPrevData.timestamp}); - - AIRTIME.showbuilder.fnAdd(aMediaIds, aSchedIds); - }; - - fnMove = function() { - var aSelect = [], - aAfter = []; - - aSelect.push({"id": aItemData[0].id, "instance": aItemData[0].instance, "timestamp": aItemData[0].timestamp}); - aAfter.push({"id": oPrevData.id, "instance": oPrevData.instance, "timestamp": oPrevData.timestamp}); - - AIRTIME.showbuilder.fnMove(aSelect, aAfter); - }; - - fnReceive = function(event, ui) { - var aItems = [], - oLibTT = TableTools.fnGetInstance('library_display'); - - aItems = oLibTT.fnGetSelectedData(); - - //if nothing is checked select the dragged item. - if (aItems.length === 0) { - aItems.push(ui.item.data("aData")); - } - - origTrs = aItems; - html = ui.helper.html(); - }; - - fnUpdate = function(event, ui) { - var prev = ui.item.prev(); - - //can't add items outside of shows. - if (!prev.hasClass("sb-allowed")) { - alert("Cannot schedule outside a show."); - ui.item.remove(); - return; - } - - aItemData = []; - oPrevData = prev.data("aData"); - - //item was dragged in - if (origTrs !== undefined) { - - $("#show_builder_table tr.ui-draggable") - .empty() - .after(html); - - aItemData = origTrs; - origTrs = undefined; - fnAdd(); - } - //item was reordered. else { - aItemData.push(ui.item.data("aData")); - fnMove(); + tr.addClass(cursorSelClass); } - }; + + //check if add button can still be enabled. + AIRTIME.library.events.enableAddButtonCheck(); + + return false; + }); - return { - placeholder: "placeholder show-builder-placeholder ui-state-highlight", - forcePlaceholderSize: true, - items: 'tr:not(:first, :last, .sb-header, .sb-footer, .sb-not-allowed)', - receive: fnReceive, - update: fnUpdate - }; - }()); + }; - tableDiv.sortable(sortableConf); + mod.init = function(oTable) { + oSchedTable = oTable; + }; - $("#show_builder .fg-toolbar") - .append('
    ') - .click(fnRemoveSelectedItems); + return AIRTIME; - //set things like a reference to the table. - AIRTIME.showbuilder.init(oTable); - - //add event to cursors. - tableDiv.find("tbody").on("click", "div.marker", function(event){ - var tr = $(this).parents("tr"), - cursorSelClass = "cursor-selected-row"; - - if (tr.hasClass(cursorSelClass)) { - tr.removeClass(cursorSelClass); - } - else { - tr.addClass(cursorSelClass); - } - - //check if add button can still be enabled. - AIRTIME.library.events.enableAddButtonCheck(); - - return false; - }); - -}); +}(AIRTIME || {})); \ No newline at end of file diff --git a/airtime_mvc/public/js/airtime/showbuilder/main_builder.js b/airtime_mvc/public/js/airtime/showbuilder/main_builder.js new file mode 100644 index 000000000..e1cc1f1c0 --- /dev/null +++ b/airtime_mvc/public/js/airtime/showbuilder/main_builder.js @@ -0,0 +1,128 @@ +$(document).ready(function(){ + + var oBaseDatePickerSettings, + oBaseTimePickerSettings, + oRange; + + oBaseDatePickerSettings = { + dateFormat: 'yy-mm-dd', + onSelect: function(sDate, oDatePicker) { + var oDate, + dInput; + + dInput = $(this); + oDate = dInput.datepicker( "setDate", sDate ); + } + }; + + oBaseTimePickerSettings = { + showPeriodLabels: false, + showCloseButton: true, + showLeadingZero: false, + defaultTime: '0:00' + }; + + /* + * Get the schedule range start in unix timestamp form (in seconds). + * defaults to NOW if nothing is selected. + * + * @param String sDatePickerId + * + * @param String sTimePickerId + * + * @return Number iTime + */ + function fnGetTimestamp(sDatePickerId, sTimePickerId) { + var date, + time, + iTime, + iServerOffset, + iClientOffset; + + if ($(sDatePickerId).val() === "") { + return 0; + } + + date = $(sDatePickerId).val(); + time = $(sTimePickerId).val(); + + date = date.split("-"); + time = time.split(":"); + + //0 based month in js. + oDate = new Date(date[0], date[1]-1, date[2], time[0], time[1]); + + iTime = oDate.getTime(); //value is in millisec. + iTime = Math.round(iTime / 1000); + iServerOffset = serverTimezoneOffset; + iClientOffset = oDate.getTimezoneOffset() * -60;//function returns minutes + + //adjust for the fact the the Date object is in client time. + iTime = iTime + iClientOffset + iServerOffset; + + return iTime; + } + /* + * Returns an object containing a unix timestamp in seconds for the start/end range + * + * @return Object {"start", "end", "range"} + */ + function fnGetScheduleRange() { + var iStart, + iEnd, + iRange, + DEFAULT_RANGE = 60*60*24; + + iStart = fnGetTimestamp("#sb_date_start", "#sb_time_start"); + iEnd = fnGetTimestamp("#sb_date_end", "#sb_time_end"); + + iRange = iEnd - iStart; + + if (iRange === 0 || iEnd < iStart) { + iEnd = iStart + DEFAULT_RANGE; + iRange = DEFAULT_RANGE; + } + + return { + start: iStart, + end: iEnd, + range: iRange + }; + } + + $("#sb_date_start").datepicker(oBaseDatePickerSettings); + $("#sb_time_start").timepicker(oBaseTimePickerSettings); + $("#sb_date_end").datepicker(oBaseDatePickerSettings); + $("#sb_time_end").timepicker(oBaseTimePickerSettings); + + $("#sb_submit").click(function(ev){ + var fn, + oRange, + op, + oTable = $('#show_builder_table').dataTable(); + + oRange = fnGetScheduleRange(); + + fn = oTable.fnSettings().fnServerData; + fn.start = oRange.start; + fn.end = oRange.end; + + op = $("div.sb-advanced-options"); + if (op.is(":visible")) { + + if (fn.ops === undefined) { + fn.ops = {}; + } + fn.ops.showFilter = op.find("#sb_show_filter").val(); + fn.ops.myShows = op.find("#sb_my_shows").is(":checked") ? 1 : 0; + } + + oTable.fnDraw(); + }); + + oRange = fnGetScheduleRange(); + AIRTIME.showbuilder.fnServerData.start = oRange.start; + AIRTIME.showbuilder.fnServerData.end = oRange.end; + + AIRTIME.showbuilder.builderDataTable(); +}); \ No newline at end of file From 2790b49ea4c241c1f02dd7c861f26c99d5b435d0 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 29 Feb 2012 19:19:02 -0500 Subject: [PATCH 28/39] CC-3364: Dev feature: Create simple script that creates and populates a show 15 seconds on the future. --- dev_tools/auto_schedule_show.php | 149 +++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 dev_tools/auto_schedule_show.php diff --git a/dev_tools/auto_schedule_show.php b/dev_tools/auto_schedule_show.php new file mode 100644 index 000000000..1419c77d8 --- /dev/null +++ b/dev_tools/auto_schedule_show.php @@ -0,0 +1,149 @@ +format("Y-m-d H:i:s"); + + + $columns = "(starts, ends, show_id, record, rebroadcast, instance_id, file_id, time_filled, last_scheduled, modified_instance)"; + $values = "('$starts', '$ends', $show_id, 0, 0, NULL, NULL, '$file[length]', '$now', 'f')"; + $query = "INSERT INTO cc_show_instances $columns values $values "; + echo $query.PHP_EOL; + + $result = query($conn, $query); + + $query = "SELECT currval('cc_show_instances_id_seq');"; + $result = pg_query($conn, $query); + if (!$result) { + echo "Error executing query $query.\n"; + exit(1); + } + + while ($row = pg_fetch_array($result)) { + $show_instance_id = $row["currval"]; + } + + return $show_instance_id; +} + +/* + * id | starts | ends | file_id | clip_length| fade_in | fade_out | cue_in | cue_out | media_item_played | instance_id + * 1 | 2012-02-29 23:25:00 | 2012-02-29 23:30:05.037166 | 1 | 00:05:05.037166 | 00:00:00 | 00:00:00 | 00:00:00 | 00:05:05.037166 | f | 5 + */ +function insertIntoCcSchedule($conn, $file, $show_instance_id, $starts, $ends){ + $columns = "(starts, ends, file_id, clip_length, fade_in, fade_out, cue_in, cue_out, media_item_played, instance_id)"; + $values = "('$starts', '$ends', $file[id], '$file[length]', '00:00:00', '00:00:00', '00:00:00', '$file[length]', 'f', $show_instance_id)"; + $query = "INSERT INTO cc_schedule $columns VALUES $values"; + echo $query.PHP_EOL; + + $result = query($conn, $query); +} + +$conn = pg_connect("host=localhost port=5432 dbname=airtime user=airtime password=airtime"); +if (!$conn) { + echo "Couldn't connect to Airtime DB.\n"; + exit(1); +} + +if (count($argv) > 1){ + if ($argv[1] == "--clean"){ + $tables = array("cc_schedule", "cc_show_instances", "cc_show"); + + foreach($tables as $table){ + $query = "DELETE FROM $table"; + echo $query.PHP_EOL; + query($conn, $query); + } + + exit(0); + } else { + $str = <<format("Y-m-d H:i:s"); +$ends = $endDateTime->format("Y-m-d H:i:s"); + +$file = getFileFromCcFiles($conn); +$show_id = insertIntoCcShow($conn); +$show_instance_id = insertIntoCcShowInstances($conn, $show_id, $starts, $ends, $file); +insertIntoCcSchedule($conn, $file, $show_instance_id, $starts, $ends); From 08048cd403a4ce261981756f4f7f4e03f288ae25 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 29 Feb 2012 20:51:46 -0500 Subject: [PATCH 29/39] CC-3374: Ability to use eclipse to run/debug our python services. Script to prepare environment and make this possible. -done --- dev_tools/toggle-pypo-debug.sh | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 dev_tools/toggle-pypo-debug.sh diff --git a/dev_tools/toggle-pypo-debug.sh b/dev_tools/toggle-pypo-debug.sh new file mode 100755 index 000000000..982c7435c --- /dev/null +++ b/dev_tools/toggle-pypo-debug.sh @@ -0,0 +1,39 @@ +#!/bin/bash +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root." 1>&2 + exit 1 +fi + +usage () { + echo "Use --enable or --disable flag. Enable is to set up environment" + echo "for specified user. --disable is to reset it back to pypo user" +} + +if [ "$1" = "--enable" ]; then + + /etc/init.d/airtime-playout stop + /etc/init.d/airtime-playout start-liquidsoap + + user=$2 + + echo "Changing ownership to user $1" + chown -Rv $user:$user /var/log/airtime/pypo + chown -v $user:$user /etc/airtime/pypo.cfg + chown -Rv $user:$user /var/tmp/airtime/pypo/ + chmod -v a+r /etc/airtime/api_client.cfg +elif [ "$1" = "--disable" ]; then + + user="pypo" + + echo "Changing ownership to user $1" + chown -Rv $user:$user /var/log/airtime/pypo + chown -v $user:$user /etc/airtime/pypo.cfg + chown -Rv $user:$user /var/tmp/airtime/pypo/ + chmod -v a+r /etc/airtime/api_client.cfg + + + /etc/init.d/airtime-playout stop-liquidsoap + /etc/init.d/airtime-playout start +else + usage +fi From 92f19139b9eae853a6bd9b8d22c7cfd50504e4dd Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 29 Feb 2012 21:27:42 -0500 Subject: [PATCH 30/39] CC-3336: Refactor schedule API used by pypo -done --- .../application/controllers/ApiController.php | 24 ++++++++++++ python_apps/api_clients/api_client.py | 34 ----------------- python_apps/pypo/pypo-notify.py | 2 +- python_apps/pypo/pypofetch.py | 13 +------ python_apps/pypo/pypopush.py | 38 +++++++++---------- 5 files changed, 46 insertions(+), 65 deletions(-) diff --git a/airtime_mvc/application/controllers/ApiController.php b/airtime_mvc/application/controllers/ApiController.php index 20e9a0f0d..fbed8005d 100644 --- a/airtime_mvc/application/controllers/ApiController.php +++ b/airtime_mvc/application/controllers/ApiController.php @@ -27,6 +27,7 @@ class ApiController extends Zend_Controller_Action ->addActionContext('live-chat', 'json') ->addActionContext('update-file-system-mount', 'json') ->addActionContext('handle-watched-dir-missing', 'json') + ->addActionContext('rabbitmq-do-push', 'json') ->initContext(); } @@ -318,6 +319,7 @@ class ApiController extends Zend_Controller_Action } } +/* public function notifyScheduleGroupPlayAction() { global $CC_CONFIG; @@ -357,6 +359,7 @@ class ApiController extends Zend_Controller_Action exit; } } + */ public function recordedShowsAction() { @@ -903,5 +906,26 @@ class ApiController extends Zend_Controller_Action $dir = base64_decode($request->getParam('dir')); Application_Model_MusicDir::removeWatchedDir($dir, false); } + + + /* This action is for use by our dev scripts, that make + * a change to the database and we want rabbitmq to send + * out a message to pypo that a potential change has been made. */ + public function rabbitmqDoPushAction(){ + global $CC_CONFIG; + + $request = $this->getRequest(); + $api_key = $request->getParam('api_key'); + if (!in_array($api_key, $CC_CONFIG["apiKey"])) + { + header('HTTP/1.0 401 Unauthorized'); + print 'You are not allowed to access this resource.'; + exit; + } + + Logging::log("Notifying RabbitMQ to send message to pypo"); + + Application_Model_RabbitMq::PushSchedule(); + } } diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py index df5b9cd1f..1148dcdc1 100755 --- a/python_apps/api_clients/api_client.py +++ b/python_apps/api_clients/api_client.py @@ -81,14 +81,6 @@ class ApiClientInterface: def get_media(self, src, dst): pass - # Implementation: optional - # - # Called from: push loop - # - # Tell server that the scheduled *playlist* has started. - def notify_scheduled_item_start_playing(self, pkey, schedule): - pass - # Implementation: optional # You dont actually have to implement this function for the liquidsoap playout to work. # @@ -285,32 +277,6 @@ class AirTimeApiClient(ApiClientInterface): except Exception, e: logger.error("%s", e) - - """ - Tell server that the scheduled *playlist* has started. - """ - def notify_scheduled_item_start_playing(self, pkey, schedule): - logger = self.logger - playlist = schedule[pkey] - schedule_id = playlist["schedule_id"] - url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_item_url"]) - - url = url.replace("%%schedule_id%%", str(schedule_id)) - logger.debug(url) - url = url.replace("%%api_key%%", self.config["api_key"]) - - try: - response = urllib.urlopen(url) - response = json.loads(response.read()) - logger.info("API-Status %s", response['status']) - logger.info("API-Message %s", response['message']) - - except Exception, e: - logger.error("Unable to connect - %s", e) - - return response - - """ This is a callback from liquidsoap, we use this to notify about the currently playing *song*. We get passed a JSON string which we handed to diff --git a/python_apps/pypo/pypo-notify.py b/python_apps/pypo/pypo-notify.py index 42fb523fc..ef480691f 100644 --- a/python_apps/pypo/pypo-notify.py +++ b/python_apps/pypo/pypo-notify.py @@ -34,7 +34,7 @@ import json from configobj import ConfigObj # custom imports -from util import * +#from util import * from api_clients import * # Set up command-line options diff --git a/python_apps/pypo/pypofetch.py b/python_apps/pypo/pypofetch.py index 99a06d6d5..fbdf1ea2a 100644 --- a/python_apps/pypo/pypofetch.py +++ b/python_apps/pypo/pypofetch.py @@ -336,7 +336,7 @@ class PypoFetch(Thread): if self.handle_media_file(media_item, dst): entry = self.create_liquidsoap_annotation(media_item, dst) - entry['show_name'] = "TODO" + media_item['show_name'] = "TODO" media_item["annotation"] = entry except Exception, e: @@ -346,7 +346,7 @@ class PypoFetch(Thread): def create_liquidsoap_annotation(self, media, dst): - pl_entry = \ + entry = \ 'annotate:media_id="%s",liq_start_next="%s",liq_fade_in="%s",liq_fade_out="%s",liq_cue_in="%s",liq_cue_out="%s",schedule_table_id="%s":%s' \ % (media['id'], 0, \ float(media['fade_in']) / 1000, \ @@ -355,15 +355,6 @@ class PypoFetch(Thread): float(media['cue_out']), \ media['row_id'], dst) - """ - Tracks are only added to the playlist if they are accessible - on the file system and larger than 0 bytes. - So this can lead to playlists shorter than expectet. - (there is a hardware silence detector for this cases...) - """ - entry = dict() - entry['type'] = 'file' - entry['annotate'] = pl_entry return entry def check_for_previous_crash(self, media_item): diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index aaddb69da..db153cd36 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -39,9 +39,11 @@ class PypoPush(Thread): self.media = dict() self.liquidsoap_state_play = True - self.push_ahead = 30 + self.push_ahead = 10 self.last_end_time = 0 + self.logger = logging.getLogger('push') + def push(self): """ The Push Loop - the push loop periodically checks if there is a playlist @@ -49,7 +51,6 @@ class PypoPush(Thread): If yes, the current liquidsoap playlist gets replaced with the corresponding one, then liquidsoap is asked (via telnet) to reload and immediately play it. """ - logger = logging.getLogger('push') timenow = time.time() # get a new schedule from pypo-fetch @@ -57,8 +58,8 @@ class PypoPush(Thread): # make sure we get the latest schedule while not self.queue.empty(): self.media = self.queue.get() - logger.debug("Received data from pypo-fetch") - logger.debug('media %s' % json.dumps(self.media)) + self.logger.debug("Received data from pypo-fetch") + self.logger.debug('media %s' % json.dumps(self.media)) media = self.media @@ -77,17 +78,13 @@ class PypoPush(Thread): """ If the media item starts in the next 30 seconds, push it to the queue. """ - logger.debug('Preparing to push media item scheduled at: %s', key) + self.logger.debug('Preparing to push media item scheduled at: %s', key) if self.push_to_liquidsoap(media_item): - logger.debug("Pushed to liquidsoap, updating 'played' status.") + self.logger.debug("Pushed to liquidsoap, updating 'played' status.") currently_on_air = True self.liquidsoap_state_play = True - - # Call API to update schedule states - logger.debug("Doing callback to server to update 'played' status.") - self.api_client.notify_scheduled_item_start_playing(key, schedule) def push_to_liquidsoap(self, media_item): """ @@ -133,15 +130,15 @@ class PypoPush(Thread): #Return the time as a floating point number expressed in seconds since the epoch, in UTC. epoch_now = time.time() - logger.debug("Epoch start: %s" % epoch_start) - logger.debug("Epoch now: %s" % epoch_now) + self.logger.debug("Epoch start: %s" % epoch_start) + self.logger.debug("Epoch now: %s" % epoch_now) sleep_time = epoch_start - epoch_now if sleep_time < 0: sleep_time = 0 - logger.debug('sleeping for %s s' % (sleep_time)) + self.logger.debug('sleeping for %s s' % (sleep_time)) time.sleep(sleep_time) def telnet_to_liquidsoap(self, media_item): @@ -156,25 +153,28 @@ class PypoPush(Thread): #tn.write(("vars.pypo_data %s\n"%liquidsoap_data["schedule_id"]).encode('utf-8')) annotation = media_item['annotation'] - tn.write('queue.push %s\n' % annotation.encode('utf-8')) + msg = 'queue.push %s\n' % annotation.encode('utf-8') + tn.write(msg) + self.logger.debug(msg) show_name = media_item['show_name'] - tn.write('vars.show_name %s\n' % show_name.encode('utf-8')) + msg = 'vars.show_name %s\n' % show_name.encode('utf-8') + tn.write(msg) + self.logger.debug(msg) tn.write("exit\n") - logger.debug(tn.read_all()) + self.logger.debug(tn.read_all()) def run(self): loops = 0 heartbeat_period = math.floor(30/PUSH_INTERVAL) - logger = logging.getLogger('push') while True: if loops % heartbeat_period == 0: - logger.info("heartbeat") + self.logger.info("heartbeat") loops = 0 try: self.push() except Exception, e: - logger.error('Pypo Push Exception: %s', e) + self.logger.error('Pypo Push Exception: %s', e) time.sleep(PUSH_INTERVAL) loops += 1 From a9cdb512b9016ecd057b71b849aa7753f172b67b Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Wed, 29 Feb 2012 21:29:02 -0500 Subject: [PATCH 31/39] CC-3364: Dev feature: Create simple script that creates and populates a show 15 seconds on the future. -done --- dev_tools/auto_schedule_show.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/dev_tools/auto_schedule_show.php b/dev_tools/auto_schedule_show.php index 1419c77d8..dee0aa7d1 100644 --- a/dev_tools/auto_schedule_show.php +++ b/dev_tools/auto_schedule_show.php @@ -110,6 +110,16 @@ function insertIntoCcSchedule($conn, $file, $show_instance_id, $starts, $ends){ $result = query($conn, $query); } +function rabbitMqNotify(){ + $ini_file = parse_ini_file("/etc/airtime/airtime.conf", true); + $url = "http://localhost/api/rabbitmq-do-push/format/json/api_key/".$ini_file["general"]["api_key"]; + + echo "Contacting $url".PHP_EOL; + $ch = curl_init($url); + curl_exec($ch); + curl_close($ch); +} + $conn = pg_connect("host=localhost port=5432 dbname=airtime user=airtime password=airtime"); if (!$conn) { echo "Couldn't connect to Airtime DB.\n"; @@ -125,11 +135,11 @@ if (count($argv) > 1){ echo $query.PHP_EOL; query($conn, $query); } - + rabbitMqNotify(); exit(0); } else { $str = <<format("Y-m-d H:i:s"); $ends = $endDateTime->format("Y-m-d H:i:s"); @@ -147,3 +157,7 @@ $file = getFileFromCcFiles($conn); $show_id = insertIntoCcShow($conn); $show_instance_id = insertIntoCcShowInstances($conn, $show_id, $starts, $ends, $file); insertIntoCcSchedule($conn, $file, $show_instance_id, $starts, $ends); + +rabbitMqNotify(); + +echo PHP_EOL."Show scheduled for $starts (UTC)".PHP_EOL; From b7e5bfe4aaaa2ce8a0c15ed8a5fcbaa9bf4443fc Mon Sep 17 00:00:00 2001 From: Naomi Aro Date: Thu, 1 Mar 2012 11:48:57 +0100 Subject: [PATCH 32/39] CC-3174 : showbuilder changing cues/cliplengths to be interval column type. --- .../models/airtime/CcPlaylistcontents.php | 117 --------- .../application/models/airtime/CcSchedule.php | 110 --------- .../map/CcPlaylistcontentsTableMap.php | 6 +- .../models/airtime/map/CcScheduleTableMap.php | 6 +- .../airtime/om/BaseCcPlaylistcontents.php | 231 +++--------------- .../om/BaseCcPlaylistcontentsQuery.php | 69 ++---- .../models/airtime/om/BaseCcSchedule.php | 231 +++--------------- .../models/airtime/om/BaseCcScheduleQuery.php | 69 ++---- airtime_mvc/build/schema.xml | 12 +- airtime_mvc/build/sql/schema.sql | 12 +- 10 files changed, 132 insertions(+), 731 deletions(-) diff --git a/airtime_mvc/application/models/airtime/CcPlaylistcontents.php b/airtime_mvc/application/models/airtime/CcPlaylistcontents.php index e533d9ada..441207585 100644 --- a/airtime_mvc/application/models/airtime/CcPlaylistcontents.php +++ b/airtime_mvc/application/models/airtime/CcPlaylistcontents.php @@ -35,39 +35,6 @@ class CcPlaylistcontents extends BaseCcPlaylistcontents { return parent::getDbFadeout($format); } - /** - * Just changing the default format to return subseconds - * - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. - */ - public function getDbCuein($format = 'H:i:s.u') - { - return parent::getDbCuein($format); - } - - /** - * Just changing the default format to return subseconds - * - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. - */ - public function getDbCueout($format = 'H:i:s.u') - { - return parent::getDbCueout($format); - } - - /** - * Just changing the default format to return subseconds - * - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. - */ - public function getDbCliplength($format = 'H:i:s.u') - { - return parent::getDbCliplength($format); - } - /** * * @param String in format SS.uuuuuu, Datetime, or DateTime accepted string. @@ -124,88 +91,4 @@ class CcPlaylistcontents extends BaseCcPlaylistcontents { return $this; } // setDbFadeout() - /** - * Sets the value of [cuein] column to a normalized version of the date/time value specified. - * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. - * @return CcPlaylistcontents The current object (for fluent API support) - */ - public function setDbCuein($v) - { - if ($v instanceof DateTime) { - $dt = $v; - } - else { - try { - $dt = new DateTime($v); - } - catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } - } - - $this->cuein = $dt->format('H:i:s.u'); - $this->modifiedColumns[] = CcPlaylistcontentsPeer::CUEIN; - - return $this; - } // setDbCuein() - - /** - * Sets the value of [cueout] column to a normalized version of the date/time value specified. - * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. - * @return CcPlaylistcontents The current object (for fluent API support) - */ - public function setDbCueout($v) - { - if ($v instanceof DateTime) { - $dt = $v; - } - else { - try { - $dt = new DateTime($v); - } - catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } - } - - $this->cueout = $dt->format('H:i:s.u'); - $this->modifiedColumns[] = CcPlaylistcontentsPeer::CUEOUT; - - return $this; - } // setDbCueout() - - /** - * Sets the value of [cliplength] column to a normalized version of the date/time value specified. - * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. - * @return CcPlaylistcontents The current object (for fluent API support) - */ - public function setDbCliplength($v) - { - if ($v instanceof DateTime) { - $dt = $v; - } - else { - - try { - - $dt = new DateTime($v); - - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } - } - - $this->cliplength = $dt->format('H:i:s.u'); - $this->modifiedColumns[] = CcPlaylistcontentsPeer::CLIPLENGTH; - - return $this; - } // setDbCliplength() - - } // CcPlaylistcontents diff --git a/airtime_mvc/application/models/airtime/CcSchedule.php b/airtime_mvc/application/models/airtime/CcSchedule.php index ff222d7a3..0a15b7a3c 100644 --- a/airtime_mvc/application/models/airtime/CcSchedule.php +++ b/airtime_mvc/application/models/airtime/CcSchedule.php @@ -15,11 +15,6 @@ */ class CcSchedule extends BaseCcSchedule { - public function getDbClipLength($format = 'H:i:s.u') - { - return parent::getDbClipLength($format); - } - /** * Get the [optionally formatted] temporal [starts] column value. * @@ -104,28 +99,6 @@ class CcSchedule extends BaseCcSchedule { return parent::getDbFadeout($format); } - /** - * Just changing the default format to return subseconds - * - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. - */ - public function getDbCueIn($format = 'H:i:s.u') - { - return parent::getDbCuein($format); - } - - /** - * Just changing the default format to return subseconds - * - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. - */ - public function getDbCueOut($format = 'H:i:s.u') - { - return parent::getDbCueout($format); - } - /** * * @param String in format SS.uuuuuu, Datetime, or DateTime accepted string. @@ -182,89 +155,6 @@ class CcSchedule extends BaseCcSchedule { return $this; } // setDbFadeout() - /** - * Sets the value of [cuein] column to a normalized version of the date/time value specified. - * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. - * @return CcPlaylistcontents The current object (for fluent API support) - */ - public function setDbCueIn($v) - { - if ($v instanceof DateTime) { - $dt = $v; - } - else { - try { - $dt = new DateTime($v); - } - catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } - } - - $this->cue_in = $dt->format('H:i:s.u'); - $this->modifiedColumns[] = CcSchedulePeer::CUE_IN; - - return $this; - } // setDbCuein() - - /** - * Sets the value of [cueout] column to a normalized version of the date/time value specified. - * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. - * @return CcPlaylistcontents The current object (for fluent API support) - */ - public function setDbCueout($v) - { - if ($v instanceof DateTime) { - $dt = $v; - } - else { - try { - $dt = new DateTime($v); - } - catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } - } - - $this->cue_out = $dt->format('H:i:s.u'); - $this->modifiedColumns[] = CcSchedulePeer::CUE_OUT; - - return $this; - } // setDbCueout() - - /** - * Sets the value of [cliplength] column to a normalized version of the date/time value specified. - * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. - * @return CcPlaylistcontents The current object (for fluent API support) - */ - public function setDbClipLength($v) - { - if ($v instanceof DateTime) { - $dt = $v; - } - else { - - try { - - $dt = new DateTime($v); - - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } - } - - $this->clip_length = $dt->format('H:i:s.u'); - $this->modifiedColumns[] = CcSchedulePeer::CLIP_LENGTH; - - return $this; - } // setDbCliplength() - /** * Sets the value of [starts] column to a normalized version of the date/time value specified. * diff --git a/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php b/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php index d654fb799..c8a01608a 100644 --- a/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php +++ b/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php @@ -42,9 +42,9 @@ class CcPlaylistcontentsTableMap extends TableMap { $this->addForeignKey('PLAYLIST_ID', 'DbPlaylistId', 'INTEGER', 'cc_playlist', 'ID', false, null, null); $this->addForeignKey('FILE_ID', 'DbFileId', 'INTEGER', 'cc_files', 'ID', false, null, null); $this->addColumn('POSITION', 'DbPosition', 'INTEGER', false, null, null); - $this->addColumn('CLIPLENGTH', 'DbCliplength', 'TIME', false, null, '00:00:00'); - $this->addColumn('CUEIN', 'DbCuein', 'TIME', false, null, '00:00:00'); - $this->addColumn('CUEOUT', 'DbCueout', 'TIME', false, null, '00:00:00'); + $this->addColumn('CLIPLENGTH', 'DbCliplength', 'VARCHAR', false, null, '00:00:00'); + $this->addColumn('CUEIN', 'DbCuein', 'VARCHAR', false, null, '00:00:00'); + $this->addColumn('CUEOUT', 'DbCueout', 'VARCHAR', false, null, '00:00:00'); $this->addColumn('FADEIN', 'DbFadein', 'TIME', false, null, '00:00:00'); $this->addColumn('FADEOUT', 'DbFadeout', 'TIME', false, null, '00:00:00'); // validators diff --git a/airtime_mvc/application/models/airtime/map/CcScheduleTableMap.php b/airtime_mvc/application/models/airtime/map/CcScheduleTableMap.php index ebbe397c8..827d971a7 100644 --- a/airtime_mvc/application/models/airtime/map/CcScheduleTableMap.php +++ b/airtime_mvc/application/models/airtime/map/CcScheduleTableMap.php @@ -42,11 +42,11 @@ class CcScheduleTableMap extends TableMap { $this->addColumn('STARTS', 'DbStarts', 'TIMESTAMP', true, null, null); $this->addColumn('ENDS', 'DbEnds', 'TIMESTAMP', true, null, null); $this->addForeignKey('FILE_ID', 'DbFileId', 'INTEGER', 'cc_files', 'ID', false, null, null); - $this->addColumn('CLIP_LENGTH', 'DbClipLength', 'TIME', false, null, '00:00:00'); + $this->addColumn('CLIP_LENGTH', 'DbClipLength', 'VARCHAR', false, null, '00:00:00'); $this->addColumn('FADE_IN', 'DbFadeIn', 'TIME', false, null, '00:00:00'); $this->addColumn('FADE_OUT', 'DbFadeOut', 'TIME', false, null, '00:00:00'); - $this->addColumn('CUE_IN', 'DbCueIn', 'TIME', false, null, '00:00:00'); - $this->addColumn('CUE_OUT', 'DbCueOut', 'TIME', false, null, '00:00:00'); + $this->addColumn('CUE_IN', 'DbCueIn', 'VARCHAR', false, null, '00:00:00'); + $this->addColumn('CUE_OUT', 'DbCueOut', 'VARCHAR', false, null, '00:00:00'); $this->addColumn('MEDIA_ITEM_PLAYED', 'DbMediaItemPlayed', 'BOOLEAN', false, null, false); $this->addForeignKey('INSTANCE_ID', 'DbInstanceId', 'INTEGER', 'cc_show_instances', 'ID', true, null, null); // validators diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php index 457a9b958..8f823a25e 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php @@ -176,102 +176,33 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent } /** - * Get the [optionally formatted] temporal [cliplength] column value. + * Get the [cliplength] column value. * - * - * @param string $format The date/time format string (either date()-style or strftime()-style). - * If format is NULL, then the raw DateTime object will be returned. - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. + * @return string */ - public function getDbCliplength($format = '%X') + public function getDbCliplength() { - if ($this->cliplength === null) { - return null; - } - - - - try { - $dt = new DateTime($this->cliplength); - } catch (Exception $x) { - throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->cliplength, true), $x); - } - - if ($format === null) { - // Because propel.useDateTimeClass is TRUE, we return a DateTime object. - return $dt; - } elseif (strpos($format, '%') !== false) { - return strftime($format, $dt->format('U')); - } else { - return $dt->format($format); - } + return $this->cliplength; } /** - * Get the [optionally formatted] temporal [cuein] column value. + * Get the [cuein] column value. * - * - * @param string $format The date/time format string (either date()-style or strftime()-style). - * If format is NULL, then the raw DateTime object will be returned. - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. + * @return string */ - public function getDbCuein($format = '%X') + public function getDbCuein() { - if ($this->cuein === null) { - return null; - } - - - - try { - $dt = new DateTime($this->cuein); - } catch (Exception $x) { - throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->cuein, true), $x); - } - - if ($format === null) { - // Because propel.useDateTimeClass is TRUE, we return a DateTime object. - return $dt; - } elseif (strpos($format, '%') !== false) { - return strftime($format, $dt->format('U')); - } else { - return $dt->format($format); - } + return $this->cuein; } /** - * Get the [optionally formatted] temporal [cueout] column value. + * Get the [cueout] column value. * - * - * @param string $format The date/time format string (either date()-style or strftime()-style). - * If format is NULL, then the raw DateTime object will be returned. - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. + * @return string */ - public function getDbCueout($format = '%X') + public function getDbCueout() { - if ($this->cueout === null) { - return null; - } - - - - try { - $dt = new DateTime($this->cueout); - } catch (Exception $x) { - throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->cueout, true), $x); - } - - if ($format === null) { - // Because propel.useDateTimeClass is TRUE, we return a DateTime object. - return $dt; - } elseif (strpos($format, '%') !== false) { - return strftime($format, $dt->format('U')); - } else { - return $dt->format($format); - } + return $this->cueout; } /** @@ -429,151 +360,61 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent } // setDbPosition() /** - * Sets the value of [cliplength] column to a normalized version of the date/time value specified. + * Set the value of [cliplength] column. * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. + * @param string $v new value * @return CcPlaylistcontents The current object (for fluent API support) */ public function setDbCliplength($v) { - // we treat '' as NULL for temporal objects because DateTime('') == DateTime('now') - // -- which is unexpected, to say the least. - if ($v === null || $v === '') { - $dt = null; - } elseif ($v instanceof DateTime) { - $dt = $v; - } else { - // some string/numeric value passed; we normalize that so that we can - // validate it. - try { - if (is_numeric($v)) { // if it's a unix timestamp - $dt = new DateTime('@'.$v, new DateTimeZone('UTC')); - // We have to explicitly specify and then change the time zone because of a - // DateTime bug: http://bugs.php.net/bug.php?id=43003 - $dt->setTimeZone(new DateTimeZone(date_default_timezone_get())); - } else { - $dt = new DateTime($v); - } - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } + if ($v !== null) { + $v = (string) $v; } - if ( $this->cliplength !== null || $dt !== null ) { - // (nested ifs are a little easier to read in this case) - - $currNorm = ($this->cliplength !== null && $tmpDt = new DateTime($this->cliplength)) ? $tmpDt->format('H:i:s') : null; - $newNorm = ($dt !== null) ? $dt->format('H:i:s') : null; - - if ( ($currNorm !== $newNorm) // normalized values don't match - || ($dt->format('H:i:s') === '00:00:00') // or the entered value matches the default - ) - { - $this->cliplength = ($dt ? $dt->format('H:i:s') : null); - $this->modifiedColumns[] = CcPlaylistcontentsPeer::CLIPLENGTH; - } - } // if either are not null + if ($this->cliplength !== $v || $this->isNew()) { + $this->cliplength = $v; + $this->modifiedColumns[] = CcPlaylistcontentsPeer::CLIPLENGTH; + } return $this; } // setDbCliplength() /** - * Sets the value of [cuein] column to a normalized version of the date/time value specified. + * Set the value of [cuein] column. * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. + * @param string $v new value * @return CcPlaylistcontents The current object (for fluent API support) */ public function setDbCuein($v) { - // we treat '' as NULL for temporal objects because DateTime('') == DateTime('now') - // -- which is unexpected, to say the least. - if ($v === null || $v === '') { - $dt = null; - } elseif ($v instanceof DateTime) { - $dt = $v; - } else { - // some string/numeric value passed; we normalize that so that we can - // validate it. - try { - if (is_numeric($v)) { // if it's a unix timestamp - $dt = new DateTime('@'.$v, new DateTimeZone('UTC')); - // We have to explicitly specify and then change the time zone because of a - // DateTime bug: http://bugs.php.net/bug.php?id=43003 - $dt->setTimeZone(new DateTimeZone(date_default_timezone_get())); - } else { - $dt = new DateTime($v); - } - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } + if ($v !== null) { + $v = (string) $v; } - if ( $this->cuein !== null || $dt !== null ) { - // (nested ifs are a little easier to read in this case) - - $currNorm = ($this->cuein !== null && $tmpDt = new DateTime($this->cuein)) ? $tmpDt->format('H:i:s') : null; - $newNorm = ($dt !== null) ? $dt->format('H:i:s') : null; - - if ( ($currNorm !== $newNorm) // normalized values don't match - || ($dt->format('H:i:s') === '00:00:00') // or the entered value matches the default - ) - { - $this->cuein = ($dt ? $dt->format('H:i:s') : null); - $this->modifiedColumns[] = CcPlaylistcontentsPeer::CUEIN; - } - } // if either are not null + if ($this->cuein !== $v || $this->isNew()) { + $this->cuein = $v; + $this->modifiedColumns[] = CcPlaylistcontentsPeer::CUEIN; + } return $this; } // setDbCuein() /** - * Sets the value of [cueout] column to a normalized version of the date/time value specified. + * Set the value of [cueout] column. * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. + * @param string $v new value * @return CcPlaylistcontents The current object (for fluent API support) */ public function setDbCueout($v) { - // we treat '' as NULL for temporal objects because DateTime('') == DateTime('now') - // -- which is unexpected, to say the least. - if ($v === null || $v === '') { - $dt = null; - } elseif ($v instanceof DateTime) { - $dt = $v; - } else { - // some string/numeric value passed; we normalize that so that we can - // validate it. - try { - if (is_numeric($v)) { // if it's a unix timestamp - $dt = new DateTime('@'.$v, new DateTimeZone('UTC')); - // We have to explicitly specify and then change the time zone because of a - // DateTime bug: http://bugs.php.net/bug.php?id=43003 - $dt->setTimeZone(new DateTimeZone(date_default_timezone_get())); - } else { - $dt = new DateTime($v); - } - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } + if ($v !== null) { + $v = (string) $v; } - if ( $this->cueout !== null || $dt !== null ) { - // (nested ifs are a little easier to read in this case) - - $currNorm = ($this->cueout !== null && $tmpDt = new DateTime($this->cueout)) ? $tmpDt->format('H:i:s') : null; - $newNorm = ($dt !== null) ? $dt->format('H:i:s') : null; - - if ( ($currNorm !== $newNorm) // normalized values don't match - || ($dt->format('H:i:s') === '00:00:00') // or the entered value matches the default - ) - { - $this->cueout = ($dt ? $dt->format('H:i:s') : null); - $this->modifiedColumns[] = CcPlaylistcontentsPeer::CUEOUT; - } - } // if either are not null + if ($this->cueout !== $v || $this->isNew()) { + $this->cueout = $v; + $this->modifiedColumns[] = CcPlaylistcontentsPeer::CUEOUT; + } return $this; } // setDbCueout() diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php index 85262c7bd..8769dfe60 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php @@ -282,29 +282,20 @@ abstract class BaseCcPlaylistcontentsQuery extends ModelCriteria /** * Filter the query on the cliplength column * - * @param string|array $dbCliplength The value to use as filter. - * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $dbCliplength The value to use as filter. + * Accepts wildcards (* and % trigger a LIKE) * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL * * @return CcPlaylistcontentsQuery The current query, for fluid interface */ public function filterByDbCliplength($dbCliplength = null, $comparison = null) { - if (is_array($dbCliplength)) { - $useMinMax = false; - if (isset($dbCliplength['min'])) { - $this->addUsingAlias(CcPlaylistcontentsPeer::CLIPLENGTH, $dbCliplength['min'], Criteria::GREATER_EQUAL); - $useMinMax = true; - } - if (isset($dbCliplength['max'])) { - $this->addUsingAlias(CcPlaylistcontentsPeer::CLIPLENGTH, $dbCliplength['max'], Criteria::LESS_EQUAL); - $useMinMax = true; - } - if ($useMinMax) { - return $this; - } - if (null === $comparison) { + if (null === $comparison) { + if (is_array($dbCliplength)) { $comparison = Criteria::IN; + } elseif (preg_match('/[\%\*]/', $dbCliplength)) { + $dbCliplength = str_replace('*', '%', $dbCliplength); + $comparison = Criteria::LIKE; } } return $this->addUsingAlias(CcPlaylistcontentsPeer::CLIPLENGTH, $dbCliplength, $comparison); @@ -313,29 +304,20 @@ abstract class BaseCcPlaylistcontentsQuery extends ModelCriteria /** * Filter the query on the cuein column * - * @param string|array $dbCuein The value to use as filter. - * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $dbCuein The value to use as filter. + * Accepts wildcards (* and % trigger a LIKE) * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL * * @return CcPlaylistcontentsQuery The current query, for fluid interface */ public function filterByDbCuein($dbCuein = null, $comparison = null) { - if (is_array($dbCuein)) { - $useMinMax = false; - if (isset($dbCuein['min'])) { - $this->addUsingAlias(CcPlaylistcontentsPeer::CUEIN, $dbCuein['min'], Criteria::GREATER_EQUAL); - $useMinMax = true; - } - if (isset($dbCuein['max'])) { - $this->addUsingAlias(CcPlaylistcontentsPeer::CUEIN, $dbCuein['max'], Criteria::LESS_EQUAL); - $useMinMax = true; - } - if ($useMinMax) { - return $this; - } - if (null === $comparison) { + if (null === $comparison) { + if (is_array($dbCuein)) { $comparison = Criteria::IN; + } elseif (preg_match('/[\%\*]/', $dbCuein)) { + $dbCuein = str_replace('*', '%', $dbCuein); + $comparison = Criteria::LIKE; } } return $this->addUsingAlias(CcPlaylistcontentsPeer::CUEIN, $dbCuein, $comparison); @@ -344,29 +326,20 @@ abstract class BaseCcPlaylistcontentsQuery extends ModelCriteria /** * Filter the query on the cueout column * - * @param string|array $dbCueout The value to use as filter. - * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $dbCueout The value to use as filter. + * Accepts wildcards (* and % trigger a LIKE) * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL * * @return CcPlaylistcontentsQuery The current query, for fluid interface */ public function filterByDbCueout($dbCueout = null, $comparison = null) { - if (is_array($dbCueout)) { - $useMinMax = false; - if (isset($dbCueout['min'])) { - $this->addUsingAlias(CcPlaylistcontentsPeer::CUEOUT, $dbCueout['min'], Criteria::GREATER_EQUAL); - $useMinMax = true; - } - if (isset($dbCueout['max'])) { - $this->addUsingAlias(CcPlaylistcontentsPeer::CUEOUT, $dbCueout['max'], Criteria::LESS_EQUAL); - $useMinMax = true; - } - if ($useMinMax) { - return $this; - } - if (null === $comparison) { + if (null === $comparison) { + if (is_array($dbCueout)) { $comparison = Criteria::IN; + } elseif (preg_match('/[\%\*]/', $dbCueout)) { + $dbCueout = str_replace('*', '%', $dbCueout); + $comparison = Criteria::LIKE; } } return $this->addUsingAlias(CcPlaylistcontentsPeer::CUEOUT, $dbCueout, $comparison); diff --git a/airtime_mvc/application/models/airtime/om/BaseCcSchedule.php b/airtime_mvc/application/models/airtime/om/BaseCcSchedule.php index 751babc2d..fa7e410dc 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcSchedule.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcSchedule.php @@ -236,36 +236,13 @@ abstract class BaseCcSchedule extends BaseObject implements Persistent } /** - * Get the [optionally formatted] temporal [clip_length] column value. + * Get the [clip_length] column value. * - * - * @param string $format The date/time format string (either date()-style or strftime()-style). - * If format is NULL, then the raw DateTime object will be returned. - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. + * @return string */ - public function getDbClipLength($format = '%X') + public function getDbClipLength() { - if ($this->clip_length === null) { - return null; - } - - - - try { - $dt = new DateTime($this->clip_length); - } catch (Exception $x) { - throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->clip_length, true), $x); - } - - if ($format === null) { - // Because propel.useDateTimeClass is TRUE, we return a DateTime object. - return $dt; - } elseif (strpos($format, '%') !== false) { - return strftime($format, $dt->format('U')); - } else { - return $dt->format($format); - } + return $this->clip_length; } /** @@ -335,69 +312,23 @@ abstract class BaseCcSchedule extends BaseObject implements Persistent } /** - * Get the [optionally formatted] temporal [cue_in] column value. + * Get the [cue_in] column value. * - * - * @param string $format The date/time format string (either date()-style or strftime()-style). - * If format is NULL, then the raw DateTime object will be returned. - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. + * @return string */ - public function getDbCueIn($format = '%X') + public function getDbCueIn() { - if ($this->cue_in === null) { - return null; - } - - - - try { - $dt = new DateTime($this->cue_in); - } catch (Exception $x) { - throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->cue_in, true), $x); - } - - if ($format === null) { - // Because propel.useDateTimeClass is TRUE, we return a DateTime object. - return $dt; - } elseif (strpos($format, '%') !== false) { - return strftime($format, $dt->format('U')); - } else { - return $dt->format($format); - } + return $this->cue_in; } /** - * Get the [optionally formatted] temporal [cue_out] column value. + * Get the [cue_out] column value. * - * - * @param string $format The date/time format string (either date()-style or strftime()-style). - * If format is NULL, then the raw DateTime object will be returned. - * @return mixed Formatted date/time value as string or DateTime object (if format is NULL), NULL if column is NULL - * @throws PropelException - if unable to parse/validate the date/time value. + * @return string */ - public function getDbCueOut($format = '%X') + public function getDbCueOut() { - if ($this->cue_out === null) { - return null; - } - - - - try { - $dt = new DateTime($this->cue_out); - } catch (Exception $x) { - throw new PropelException("Internally stored date/time/timestamp value could not be converted to DateTime: " . var_export($this->cue_out, true), $x); - } - - if ($format === null) { - // Because propel.useDateTimeClass is TRUE, we return a DateTime object. - return $dt; - } elseif (strpos($format, '%') !== false) { - return strftime($format, $dt->format('U')); - } else { - return $dt->format($format); - } + return $this->cue_out; } /** @@ -563,51 +494,21 @@ abstract class BaseCcSchedule extends BaseObject implements Persistent } // setDbFileId() /** - * Sets the value of [clip_length] column to a normalized version of the date/time value specified. + * Set the value of [clip_length] column. * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. + * @param string $v new value * @return CcSchedule The current object (for fluent API support) */ public function setDbClipLength($v) { - // we treat '' as NULL for temporal objects because DateTime('') == DateTime('now') - // -- which is unexpected, to say the least. - if ($v === null || $v === '') { - $dt = null; - } elseif ($v instanceof DateTime) { - $dt = $v; - } else { - // some string/numeric value passed; we normalize that so that we can - // validate it. - try { - if (is_numeric($v)) { // if it's a unix timestamp - $dt = new DateTime('@'.$v, new DateTimeZone('UTC')); - // We have to explicitly specify and then change the time zone because of a - // DateTime bug: http://bugs.php.net/bug.php?id=43003 - $dt->setTimeZone(new DateTimeZone(date_default_timezone_get())); - } else { - $dt = new DateTime($v); - } - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } + if ($v !== null) { + $v = (string) $v; } - if ( $this->clip_length !== null || $dt !== null ) { - // (nested ifs are a little easier to read in this case) - - $currNorm = ($this->clip_length !== null && $tmpDt = new DateTime($this->clip_length)) ? $tmpDt->format('H:i:s') : null; - $newNorm = ($dt !== null) ? $dt->format('H:i:s') : null; - - if ( ($currNorm !== $newNorm) // normalized values don't match - || ($dt->format('H:i:s') === '00:00:00') // or the entered value matches the default - ) - { - $this->clip_length = ($dt ? $dt->format('H:i:s') : null); - $this->modifiedColumns[] = CcSchedulePeer::CLIP_LENGTH; - } - } // if either are not null + if ($this->clip_length !== $v || $this->isNew()) { + $this->clip_length = $v; + $this->modifiedColumns[] = CcSchedulePeer::CLIP_LENGTH; + } return $this; } // setDbClipLength() @@ -713,101 +614,41 @@ abstract class BaseCcSchedule extends BaseObject implements Persistent } // setDbFadeOut() /** - * Sets the value of [cue_in] column to a normalized version of the date/time value specified. + * Set the value of [cue_in] column. * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. + * @param string $v new value * @return CcSchedule The current object (for fluent API support) */ public function setDbCueIn($v) { - // we treat '' as NULL for temporal objects because DateTime('') == DateTime('now') - // -- which is unexpected, to say the least. - if ($v === null || $v === '') { - $dt = null; - } elseif ($v instanceof DateTime) { - $dt = $v; - } else { - // some string/numeric value passed; we normalize that so that we can - // validate it. - try { - if (is_numeric($v)) { // if it's a unix timestamp - $dt = new DateTime('@'.$v, new DateTimeZone('UTC')); - // We have to explicitly specify and then change the time zone because of a - // DateTime bug: http://bugs.php.net/bug.php?id=43003 - $dt->setTimeZone(new DateTimeZone(date_default_timezone_get())); - } else { - $dt = new DateTime($v); - } - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } + if ($v !== null) { + $v = (string) $v; } - if ( $this->cue_in !== null || $dt !== null ) { - // (nested ifs are a little easier to read in this case) - - $currNorm = ($this->cue_in !== null && $tmpDt = new DateTime($this->cue_in)) ? $tmpDt->format('H:i:s') : null; - $newNorm = ($dt !== null) ? $dt->format('H:i:s') : null; - - if ( ($currNorm !== $newNorm) // normalized values don't match - || ($dt->format('H:i:s') === '00:00:00') // or the entered value matches the default - ) - { - $this->cue_in = ($dt ? $dt->format('H:i:s') : null); - $this->modifiedColumns[] = CcSchedulePeer::CUE_IN; - } - } // if either are not null + if ($this->cue_in !== $v || $this->isNew()) { + $this->cue_in = $v; + $this->modifiedColumns[] = CcSchedulePeer::CUE_IN; + } return $this; } // setDbCueIn() /** - * Sets the value of [cue_out] column to a normalized version of the date/time value specified. + * Set the value of [cue_out] column. * - * @param mixed $v string, integer (timestamp), or DateTime value. Empty string will - * be treated as NULL for temporal objects. + * @param string $v new value * @return CcSchedule The current object (for fluent API support) */ public function setDbCueOut($v) { - // we treat '' as NULL for temporal objects because DateTime('') == DateTime('now') - // -- which is unexpected, to say the least. - if ($v === null || $v === '') { - $dt = null; - } elseif ($v instanceof DateTime) { - $dt = $v; - } else { - // some string/numeric value passed; we normalize that so that we can - // validate it. - try { - if (is_numeric($v)) { // if it's a unix timestamp - $dt = new DateTime('@'.$v, new DateTimeZone('UTC')); - // We have to explicitly specify and then change the time zone because of a - // DateTime bug: http://bugs.php.net/bug.php?id=43003 - $dt->setTimeZone(new DateTimeZone(date_default_timezone_get())); - } else { - $dt = new DateTime($v); - } - } catch (Exception $x) { - throw new PropelException('Error parsing date/time value: ' . var_export($v, true), $x); - } + if ($v !== null) { + $v = (string) $v; } - if ( $this->cue_out !== null || $dt !== null ) { - // (nested ifs are a little easier to read in this case) - - $currNorm = ($this->cue_out !== null && $tmpDt = new DateTime($this->cue_out)) ? $tmpDt->format('H:i:s') : null; - $newNorm = ($dt !== null) ? $dt->format('H:i:s') : null; - - if ( ($currNorm !== $newNorm) // normalized values don't match - || ($dt->format('H:i:s') === '00:00:00') // or the entered value matches the default - ) - { - $this->cue_out = ($dt ? $dt->format('H:i:s') : null); - $this->modifiedColumns[] = CcSchedulePeer::CUE_OUT; - } - } // if either are not null + if ($this->cue_out !== $v || $this->isNew()) { + $this->cue_out = $v; + $this->modifiedColumns[] = CcSchedulePeer::CUE_OUT; + } return $this; } // setDbCueOut() diff --git a/airtime_mvc/application/models/airtime/om/BaseCcScheduleQuery.php b/airtime_mvc/application/models/airtime/om/BaseCcScheduleQuery.php index 4c661ad56..3e7c56b25 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcScheduleQuery.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcScheduleQuery.php @@ -290,29 +290,20 @@ abstract class BaseCcScheduleQuery extends ModelCriteria /** * Filter the query on the clip_length column * - * @param string|array $dbClipLength The value to use as filter. - * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $dbClipLength The value to use as filter. + * Accepts wildcards (* and % trigger a LIKE) * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL * * @return CcScheduleQuery The current query, for fluid interface */ public function filterByDbClipLength($dbClipLength = null, $comparison = null) { - if (is_array($dbClipLength)) { - $useMinMax = false; - if (isset($dbClipLength['min'])) { - $this->addUsingAlias(CcSchedulePeer::CLIP_LENGTH, $dbClipLength['min'], Criteria::GREATER_EQUAL); - $useMinMax = true; - } - if (isset($dbClipLength['max'])) { - $this->addUsingAlias(CcSchedulePeer::CLIP_LENGTH, $dbClipLength['max'], Criteria::LESS_EQUAL); - $useMinMax = true; - } - if ($useMinMax) { - return $this; - } - if (null === $comparison) { + if (null === $comparison) { + if (is_array($dbClipLength)) { $comparison = Criteria::IN; + } elseif (preg_match('/[\%\*]/', $dbClipLength)) { + $dbClipLength = str_replace('*', '%', $dbClipLength); + $comparison = Criteria::LIKE; } } return $this->addUsingAlias(CcSchedulePeer::CLIP_LENGTH, $dbClipLength, $comparison); @@ -383,29 +374,20 @@ abstract class BaseCcScheduleQuery extends ModelCriteria /** * Filter the query on the cue_in column * - * @param string|array $dbCueIn The value to use as filter. - * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $dbCueIn The value to use as filter. + * Accepts wildcards (* and % trigger a LIKE) * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL * * @return CcScheduleQuery The current query, for fluid interface */ public function filterByDbCueIn($dbCueIn = null, $comparison = null) { - if (is_array($dbCueIn)) { - $useMinMax = false; - if (isset($dbCueIn['min'])) { - $this->addUsingAlias(CcSchedulePeer::CUE_IN, $dbCueIn['min'], Criteria::GREATER_EQUAL); - $useMinMax = true; - } - if (isset($dbCueIn['max'])) { - $this->addUsingAlias(CcSchedulePeer::CUE_IN, $dbCueIn['max'], Criteria::LESS_EQUAL); - $useMinMax = true; - } - if ($useMinMax) { - return $this; - } - if (null === $comparison) { + if (null === $comparison) { + if (is_array($dbCueIn)) { $comparison = Criteria::IN; + } elseif (preg_match('/[\%\*]/', $dbCueIn)) { + $dbCueIn = str_replace('*', '%', $dbCueIn); + $comparison = Criteria::LIKE; } } return $this->addUsingAlias(CcSchedulePeer::CUE_IN, $dbCueIn, $comparison); @@ -414,29 +396,20 @@ abstract class BaseCcScheduleQuery extends ModelCriteria /** * Filter the query on the cue_out column * - * @param string|array $dbCueOut The value to use as filter. - * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $dbCueOut The value to use as filter. + * Accepts wildcards (* and % trigger a LIKE) * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL * * @return CcScheduleQuery The current query, for fluid interface */ public function filterByDbCueOut($dbCueOut = null, $comparison = null) { - if (is_array($dbCueOut)) { - $useMinMax = false; - if (isset($dbCueOut['min'])) { - $this->addUsingAlias(CcSchedulePeer::CUE_OUT, $dbCueOut['min'], Criteria::GREATER_EQUAL); - $useMinMax = true; - } - if (isset($dbCueOut['max'])) { - $this->addUsingAlias(CcSchedulePeer::CUE_OUT, $dbCueOut['max'], Criteria::LESS_EQUAL); - $useMinMax = true; - } - if ($useMinMax) { - return $this; - } - if (null === $comparison) { + if (null === $comparison) { + if (is_array($dbCueOut)) { $comparison = Criteria::IN; + } elseif (preg_match('/[\%\*]/', $dbCueOut)) { + $dbCueOut = str_replace('*', '%', $dbCueOut); + $comparison = Criteria::LIKE; } } return $this->addUsingAlias(CcSchedulePeer::CUE_OUT, $dbCueOut, $comparison); diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml index 85aca369f..507dcc6ae 100644 --- a/airtime_mvc/build/schema.xml +++ b/airtime_mvc/build/schema.xml @@ -237,9 +237,9 @@ - - - + + + @@ -273,11 +273,11 @@ - + - - + +