From 997b5c31c3172f7c59cfbf433fb5ddabe43b1983 Mon Sep 17 00:00:00 2001 From: denise Date: Thu, 2 Aug 2012 11:52:11 -0400 Subject: [PATCH 1/4] CC-84: Smart Playlists - fixed dragging tracks into playlists (smart and dumb) - display alert when dragging into dynamic block - display alert when dragging playlist into block --- .../controllers/PlaylistController.php | 26 ++++++++++++++----- airtime_mvc/application/models/Block.php | 13 +++++++--- .../library/events/library_playlistbuilder.js | 9 ++++--- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php index 43103edf9..678a17195 100644 --- a/airtime_mvc/application/controllers/PlaylistController.php +++ b/airtime_mvc/application/controllers/PlaylistController.php @@ -127,7 +127,7 @@ class PlaylistController extends Zend_Controller_Action private function blockDynamic($obj) { - $this->view->error = "You cannot add tracks to dynamic block."; + $this->view->error = "You cannot add tracks to dynamic blocks."; $this->createFullResponse($obj); } @@ -152,6 +152,11 @@ class PlaylistController extends Zend_Controller_Action Logging::log("{$e->getLine()}"); Logging::log("{$e->getMessage()}"); } + + private function playlistDenied($obj) { + $this->view->error = "You cannot add playlists to smart playlists."; + $this->createFullResponse($obj); + } public function indexAction() { @@ -307,9 +312,15 @@ class PlaylistController extends Zend_Controller_Action $obj->addAudioClips($ids, $afterItem, $addType); } else if ($obj->isStatic()) { // if the dest is a block object + //check if any items are playlists + foreach($ids as $id) { + if (is_array($id) && isset($id[1]) && $id[1] == 'playlist') { + throw new Exception('playlist to block'); + } + } $obj->addAudioClips($ids, $afterItem, $addType); } else { - throw new BlockDynamicException; + throw new Exception('track to dynamic'); } $this->createUpdateResponse($obj); } @@ -319,11 +330,14 @@ class PlaylistController extends Zend_Controller_Action catch (PlaylistNotFoundException $e) { $this->playlistNotFound($obj_type); } - catch (BlockDynamicException $e) { - $this->blockDynamic($obj); - } catch (Exception $e) { - $this->playlistUnknownError($e); + if ($e->getMessage() == 'playlist to block') { + $this->playlistDenied($obj); + } else if ($e->getMessage() == 'track to dynamic') { + $this->blockDynamic($obj); + } else { + $this->playlistUnknownError($e); + } } } diff --git a/airtime_mvc/application/models/Block.php b/airtime_mvc/application/models/Block.php index 3e4a91e4c..65fc55c80 100644 --- a/airtime_mvc/application/models/Block.php +++ b/airtime_mvc/application/models/Block.php @@ -266,7 +266,7 @@ EOT; if ($modifier == "minutes") { $timestamp = "00:".$value.":00"; } else if ($modifier == "hours") { - $timestamp = $value."00:00"; + $timestamp = $value.":00:00.0"; } $formatter = new LengthFormatter($timestamp); $length = "~".$formatter->format(); @@ -388,9 +388,14 @@ EOT; foreach ($p_items as $ac) { Logging::log("Adding audio file {$ac}"); - - $res = $this->insertBlockElement($this->buildEntry($ac, $pos)); - $pos = $pos + 1; + + if (is_array($ac) && $ac[1] == 'audioclip') { + $res = $this->insertBlockElement($this->buildEntry($ac[0], $pos)); + $pos = $pos + 1; + } elseif (!is_array($ac)) { + $res = $this->insertBlockElement($this->buildEntry($ac, $pos)); + $pos = $pos + 1; + } } //reset the positions of the remaining items. 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 c5845ce4c..dcb63f8af 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js @@ -128,11 +128,14 @@ var AIRTIME = (function(AIRTIME){ for (i = 0, length = aData.length; i < length; i++) { temp = aData[i]; if (temp.ftype === "audioclip" || temp.ftype === "block") { - aMediaIds.push(temp.id); + aMediaIds.push(new Array (temp.id, temp.ftype)); } } - - AIRTIME.playlist.fnAddItems(aMediaIds, undefined, 'after'); + if (aMediaIds.length > 0) { + AIRTIME.playlist.fnAddItems(aMediaIds, undefined, 'after'); + } else { + alert('You cannot add playlists to smart playlists'); + } }); //delete from library. From 4fca33c5fc4fb619d28576cbcc58b1f4abb3da00 Mon Sep 17 00:00:00 2001 From: denise Date: Thu, 2 Aug 2012 12:59:00 -0400 Subject: [PATCH 2/4] CC-84: Smart Playlists - removed block from right side if it gets deleted from context menu --- airtime_mvc/public/js/airtime/library/library.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 1b262c709..cfd1be807 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -639,7 +639,7 @@ var AIRTIME = (function(AIRTIME) { //delete through the playlist controller, will reset //playlist screen if this is the currently edited playlist. - if (data.ftype === "playlist" && screen === "playlist") { + if ((data.ftype === "playlist" || data.ftype === "block") && screen === "playlist") { callback = function() { if (confirm('Are you sure you want to delete the selected item?')) { From 9cc152c0ae4fca81dcd0dc2d32d47fe8c537cd07 Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Thu, 2 Aug 2012 15:45:15 -0400 Subject: [PATCH 3/4] CC-1665: Scheduled stream rebroadcasting and recording -validate form fields of new webstream --- .../controllers/WebstreamController.php | 11 ++- airtime_mvc/application/models/Webstream.php | 78 +++++++++++++++++-- .../views/scripts/webstream/webstream.phtml | 3 + airtime_mvc/public/js/airtime/library/spl.js | 23 +++++- 4 files changed, 100 insertions(+), 15 deletions(-) diff --git a/airtime_mvc/application/controllers/WebstreamController.php b/airtime_mvc/application/controllers/WebstreamController.php index deaddfca7..2b38f50aa 100644 --- a/airtime_mvc/application/controllers/WebstreamController.php +++ b/airtime_mvc/application/controllers/WebstreamController.php @@ -33,9 +33,14 @@ class WebstreamController extends Zend_Controller_Action { $request = $this->getRequest(); - Application_Model_Webstream::save($request); + $analysis = Application_Model_Webstream::analyzeFormData($request); - $this->view->statusMessage = "
Webstream saved.
"; - + if (Application_Model_Webstream::isValid($analysis)) { + Application_Model_Webstream::save($request); + $this->view->statusMessage = "
Webstream saved.
"; + } else { + $this->view->statusMessage = "
Invalid form values.
"; + $this->view->analysis = $analysis; + } } } diff --git a/airtime_mvc/application/models/Webstream.php b/airtime_mvc/application/models/Webstream.php index 3663b489d..8ff6be973 100644 --- a/airtime_mvc/application/models/Webstream.php +++ b/airtime_mvc/application/models/Webstream.php @@ -75,20 +75,82 @@ class Application_Model_Webstream{ return $leftOvers; } +/* +Array +( + [controller] => Webstream + [action] => save + [module] => default + [format] => json + [description] => desc + [url] => http:// + [length] => 00h 20m + [name] => Default +) + */ + + + public static function analyzeFormData($request) + { + $valid = array("length" => array(true, ''), + "url" => array(true, '')); + + $length = trim($request->getParam("length")); + $result = preg_match("/^([0-9]{1,2})h ([0-5][0-9])m$/", $length, $matches); + if (!$result == 1 || !count($matches) == 3) { + $valid['length'][0] = false; + $valid['length'][1] = 'Length should be of form "00h 00m"'; + } + + + $url = trim($request->getParam("url")); + //simple validator that checks to make sure that the url starts with http(s), + //and that the domain is at least 1 letter long followed by a period. + $result = preg_match("/^(http|https):\/\/.+\./", $url, $matches); + + if ($result == 0) { + $valid['url'][0] = false; + $valid['url'][1] = 'URL should be of form "http://www.domain.com/mount"'; + } + + + $name = trim($request->getParam("name")); + if (strlen($name) == 0) { + $valid['name'][0] = false; + $valid['name'][1] = 'Webstream name cannot be empty'; + } + + return $valid; + } + + public static function isValid($analysis) + { + foreach ($analysis as $k => $v) { + if ($v[0] == false) { + return false; + } + } + + return true; + } public static function save($request) { - Logging::log($request->getParams()); $userInfo = Zend_Auth::getInstance()->getStorage()->read(); - Logging::log($userInfo); $length = trim($request->getParam("length")); - preg_match("/^([0-9]{1,2})h ([0-5][0-9])m$/", $length, $matches); - $hours = $matches[1]; - $minutes = $matches[2]; - $di = new DateInterval("PT{$hours}H{$minutes}M"); - $dblength = $di->format("%H:%I"); - + $result = preg_match("/^([0-9]{1,2})h ([0-5][0-9])m$/", $length, $matches); + if ($result == 1 && count($matches) == 3) { + $hours = $matches[1]; + $minutes = $matches[2]; + $di = new DateInterval("PT{$hours}H{$minutes}M"); + $dblength = $di->format("%H:%I"); + } else { + //This should never happen because we should have already validated + //in the controller + throw new Exception("Invalid date format: $length"); + } + #TODO: These should be validated by a Zend Form. $webstream = new CcWebstream(); $webstream->setDbName($request->getParam("name")); diff --git a/airtime_mvc/application/views/scripts/webstream/webstream.phtml b/airtime_mvc/application/views/scripts/webstream/webstream.phtml index 7e3228d70..4761f2ac9 100644 --- a/airtime_mvc/application/views/scripts/webstream/webstream.phtml +++ b/airtime_mvc/application/views/scripts/webstream/webstream.phtml @@ -8,6 +8,7 @@
+

ws->getName(); ?>

@@ -22,10 +23,12 @@ +
+
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index 8c256ca8b..6a9f41b99 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -532,15 +532,30 @@ var AIRTIME = (function(AIRTIME){ var streamurl = $pl.find("#streamurl-element input").val(); var length = $pl.find("#streamlength-element input").val(); var name = $pl.find("#playlist_name_display").text(); + + //hide any previous errors (if any) + $("#side_playlist .errors").empty().hide(); var url = 'Webstream/save'; $.post(url, {format: "json", description: description, url:streamurl, length: length, name: name}, function(json){ - var $status = $("#side_playlist .status"); - $status.html(json.statusMessage); - $status.show(); - setTimeout(function(){$status.fadeOut("slow", function(){$status.empty()})}, 5000); + if (json.analysis){ + for (var s in json.analysis){ + var field = json.analysis[s]; + + if (!field[0]) { + var elemId = "#"+s+"-error"; + var $div = $("#side_playlist " + elemId).text(field[1]).show(); + } + } + } else { + var $status = $("#side_playlist .status"); + $status.html(json.statusMessage); + $status.show(); + setTimeout(function(){$status.fadeOut("slow", function(){$status.empty()})}, 5000); + } + }); From fcce4bf64c227df4c235f9619668291b84176b7b Mon Sep 17 00:00:00 2001 From: Martin Konecny Date: Thu, 2 Aug 2012 16:26:41 -0400 Subject: [PATCH 4/4] CC-1665: Scheduled stream rebroadcasting and recording -cancelling a webstream works --- python_apps/pypo/pypopush.py | 85 ++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index a6ebef2a6..09dd7de37 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -49,6 +49,7 @@ class PypoPush(Thread): self.pushed_objects = {} self.logger = logging.getLogger('push') + self.current_stream_info = None def main(self): loops = 0 @@ -73,38 +74,54 @@ class PypoPush(Thread): tnow = datetime.utcnow() current_event_chain, original_chain = self.get_current_chain(chains, tnow) - if len(current_event_chain) > 0 and len(liquidsoap_queue_approx) == 0: - #Something is scheduled but Liquidsoap is not playing anything! - #Need to schedule it immediately..this might happen if Liquidsoap crashed. + + if len(current_event_chain) > 0: try: chains.remove(original_chain) except ValueError, e: self.logger.error(str(e)) + if len(liquidsoap_queue_approx) == 0 and current_event_chain[0]['type'] == 'file': + #Something is scheduled but Liquidsoap is not playing anything! + #Need to schedule it immediately..this might happen if Liquidsoap crashed. + self.modify_cue_point(current_event_chain[0]) + next_media_item_chain = current_event_chain + time_until_next_play = 0 + #sleep for 0.2 seconds to give pypo-file time to copy. + time.sleep(0.2) + continue + if not self.current_stream_info and current_event_chain[0]['type'] == 'stream': + #a stream is schedule but Liquidsoap is not playing it. Need to start it. + next_media_item_chain = current_event_chain + time_until_next_play = 0 + continue - self.modify_cue_point(current_event_chain[0]) - next_media_item_chain = current_event_chain - time_until_next_play = 0 - #sleep for 0.2 seconds to give pypo-file time to copy. - time.sleep(0.2) + #At this point we know that Liquidsoap is playing something, and that something + #is scheduled. We need to verify whether the schedule we just received matches + #what Liquidsoap is playing, and if not, correct it. + media_chain = filter(lambda item: (item["type"] == "file"), current_event_chain) + stream_chain = filter(lambda item: (item["type"] == "stream"), current_event_chain) + self.handle_new_schedule(media_schedule, liquidsoap_queue_approx, media_chain, stream_chain) + + + #At this point everything in the present has been taken care of and Liquidsoap + #is playing whatever is scheduled. + #Now we need to prepare ourselves for future scheduled events. + # + next_media_item_chain = self.get_next_schedule_chain(chains, tnow) + + self.logger.debug("Next schedule chain: %s", next_media_item_chain) + if next_media_item_chain is not None: + try: + chains.remove(next_media_item_chain) + except ValueError, e: + self.logger.error(str(e)) + + chain_start = datetime.strptime(next_media_item_chain[0]['start'], "%Y-%m-%d-%H-%M-%S") + time_until_next_play = self.date_interval_to_seconds(chain_start - datetime.utcnow()) + self.logger.debug("Blocking %s seconds until show start", time_until_next_play) else: - media_chain = filter(lambda item: (item["type"] == "file"), current_event_chain) - self.handle_new_media_schedule(media_schedule, liquidsoap_queue_approx, media_chain) - - next_media_item_chain = self.get_next_schedule_chain(chains, tnow) - - self.logger.debug("Next schedule chain: %s", next_media_item_chain) - if next_media_item_chain is not None: - try: - chains.remove(next_media_item_chain) - except ValueError, e: - self.logger.error(str(e)) - - chain_start = datetime.strptime(next_media_item_chain[0]['start'], "%Y-%m-%d-%H-%M-%S") - time_until_next_play = self.date_interval_to_seconds(chain_start - datetime.utcnow()) - self.logger.debug("Blocking %s seconds until show start", time_until_next_play) - else: - self.logger.debug("Blocking indefinitely since no show scheduled") - time_until_next_play = None + self.logger.debug("Blocking indefinitely since no show scheduled") + time_until_next_play = None except Empty, e: #We only get here when a new chain of tracks are ready to be played. self.push_to_liquidsoap(next_media_item_chain) @@ -166,7 +183,7 @@ class PypoPush(Thread): return liquidsoap_queue_approx - def handle_new_media_schedule(self, media_schedule, liquidsoap_queue_approx, media_chain): + def handle_new_schedule(self, media_schedule, liquidsoap_queue_approx, media_chain, stream_chain): """ This function's purpose is to gracefully handle situations where Liquidsoap already has a track in its queue, but the schedule @@ -175,6 +192,18 @@ class PypoPush(Thread): queue. """ + if self.current_stream_info: + if len(stream_chain) == 0: + #Liquidsoap is rebroadcasting a webstream, but there is no stream + #in the schedule. Let's stop streaming. + self.stop_web_stream(self.current_stream_info) + elif self.current_stream_info['uri'] != stream_chain[0]['uri']: + #Liquidsoap is rebroadcasting a webstream and a webstream is scheduled + #to play, but they are not the same! + self.stop_web_stream(self.current_stream_info) + self.start_web_stream(stream_chain[0]) + + problem_at_iteration = self.find_removed_items(media_schedule, liquidsoap_queue_approx) if problem_at_iteration is not None: @@ -388,6 +417,7 @@ class PypoPush(Thread): tn.write("exit\n") self.logger.debug(tn.read_all()) + self.current_stream_info = media_item except Exception, e: self.logger.error(str(e)) finally: @@ -406,6 +436,7 @@ class PypoPush(Thread): tn.write("exit\n") self.logger.debug(tn.read_all()) + self.current_stream_info = None except Exception, e: self.logger.error(str(e)) finally: