diff --git a/CREDITS b/CREDITS index d303519ef..2594f4dc5 100644 --- a/CREDITS +++ b/CREDITS @@ -1,3 +1,32 @@ +======= +CREDITS +======= +Version 2.2.0 +------------- +Martin Konecny (martin.konecny@sourcefabric.org) + Role: Developer Team Lead + +Naomi Aro (naomi.aro@sourcefabric.org) + Role: Software Developer + +James Moon (james.moon@sourcefabric.org) + Role: Software Developer + +Denise Rigato (denise.rigato@sourcefabric.org) + Role: Software Developer + +Rudi Grinberg (rudi.grinberg@sourcefabric.org) + Role: Software Developer + +Cliff Wang (cliff.wang@sourcefabric.org) + Role: QA + +Mikayel Karapetian (michael.karapetian@sourcefabric.org) + Role: QA + +Daniel James (daniel.james@sourcefabric.org) + Role: Documentor & QA + ======= CREDITS ======= diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php index 1d1ed55e4..eb5cb3989 100644 --- a/airtime_mvc/application/configs/constants.php +++ b/airtime_mvc/application/configs/constants.php @@ -1,40 +1,40 @@ getFileOwnerId() == $user->getId()) { $menu["del"] = array("name"=> "Delete", "icon" => "delete", "url" => "/library/delete"); $menu["edit"] = array("name"=> "Edit Metadata", "icon" => "edit", "url" => "/library/edit-file-md/id/{$id}"); } @@ -276,6 +277,7 @@ class LibraryController extends Zend_Controller_Action $streams = array(); $message = null; + $noPermissionMsg = "You don't have permission to delete selected items."; foreach ($mediaItems as $media) { @@ -293,19 +295,21 @@ class LibraryController extends Zend_Controller_Action try { Application_Model_Playlist::deletePlaylists($playlists, $user->getId()); } catch (PlaylistNoPermissionException $e) { - $this->view->message = "You don't have permission to delete selected items."; - - return; + $message = $noPermissionMsg; } try { Application_Model_Block::deleteBlocks($blocks, $user->getId()); + } catch (BlockNoPermissionException $e) { + $message = $noPermissionMsg; } catch (Exception $e) { //TODO: warn user that not all blocks could be deleted. } try { Application_Model_Webstream::deleteStreams($streams, $user->getId()); + } catch (WebstreamNoPermissionException $e) { + $message = $noPermissionMsg; } catch (Exception $e) { //TODO: warn user that not all streams could be deleted. Logging::info($e); @@ -317,7 +321,9 @@ class LibraryController extends Zend_Controller_Action if (isset($file)) { try { - $res = $file->delete(true); + $res = $file->delete(); + } catch (FileNoPermissionException $e) { + $message = $noPermissionMsg; } catch (Exception $e) { //could throw a scheduled in future exception. $message = "Could not delete some scheduled files."; @@ -364,15 +370,17 @@ class LibraryController extends Zend_Controller_Action { $user = Application_Model_User::getCurrentUser(); $isAdminOrPM = $user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER)); - if (!$isAdminOrPM) { - return; - } $request = $this->getRequest(); - $form = new Application_Form_EditAudioMD(); $file_id = $this->_getParam('id', null); $file = Application_Model_StoredFile::Recall($file_id); + + if (!$isAdminOrPM && $file->getFileOwnerId() != $user->getId()) { + return; + } + + $form = new Application_Form_EditAudioMD(); $form->populate($file->getDbColMetadata()); if ($request->isPost()) { diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php index 2cca81ac2..38dbd4fd2 100644 --- a/airtime_mvc/application/controllers/PlaylistController.php +++ b/airtime_mvc/application/controllers/PlaylistController.php @@ -513,6 +513,7 @@ class PlaylistController extends Zend_Controller_Action } catch (BlockNotFoundException $e) { $this->playlistNotFound('block', true); } catch (Exception $e) { + //Logging::info($e); $this->playlistUnknownError($e); } } diff --git a/airtime_mvc/application/forms/GeneralPreferences.php b/airtime_mvc/application/forms/GeneralPreferences.php index 9bca14349..76144df05 100644 --- a/airtime_mvc/application/forms/GeneralPreferences.php +++ b/airtime_mvc/application/forms/GeneralPreferences.php @@ -15,7 +15,7 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm $defaultFade = Application_Model_Preference::GetDefaultFade(); if ($defaultFade == "") { - $defaultFade = '0.500000'; + $defaultFade = '0.5'; } //Station name @@ -37,8 +37,8 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm 'required' => false, 'filters' => array('StringTrim'), 'validators' => array(array('regex', false, - array('/^[0-9]{1,2}(\.\d{1,6})?$/', - 'messages' => 'enter a time in seconds 0{.000000}'))), + array('/^[0-9]{1,2}(\.\d{1})?$/', + 'messages' => 'enter a time in seconds 0{.0}'))), 'value' => $defaultFade, 'decorators' => array( 'ViewHelper' diff --git a/airtime_mvc/application/forms/SmartBlockCriteria.php b/airtime_mvc/application/forms/SmartBlockCriteria.php index 55a28c79a..e48fc7590 100644 --- a/airtime_mvc/application/forms/SmartBlockCriteria.php +++ b/airtime_mvc/application/forms/SmartBlockCriteria.php @@ -212,6 +212,14 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm }//for + $repeatTracks = new Zend_Form_Element_Checkbox('sp_repeat_tracks'); + $repeatTracks->setDecorators(array('viewHelper')) + ->setLabel('Allow Repeat Tracks:'); + if (isset($storedCrit["repeat_tracks"])) { + $repeatTracks->setChecked($storedCrit["repeat_tracks"]["value"] == 1?true:false); + } + $this->addElement($repeatTracks); + $limit = new Zend_Form_Element_Select('sp_limit_options'); $limit->setAttrib('class', 'sp_input_select') ->setDecorators(array('viewHelper')) @@ -220,7 +228,7 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm $limit->setValue($storedCrit["limit"]["modifier"]); } $this->addElement($limit); - + $limitValue = new Zend_Form_Element_Text('sp_limit_value'); $limitValue->setAttrib('class', 'sp_input_text_limit') ->setLabel('Limit to') diff --git a/airtime_mvc/application/models/Block.php b/airtime_mvc/application/models/Block.php index eb1aab691..57018ad5f 100644 --- a/airtime_mvc/application/models/Block.php +++ b/airtime_mvc/application/models/Block.php @@ -308,10 +308,11 @@ SQL; $length = $value." ".$modifier; } else { $hour = "00"; + $mins = "00"; if ($modifier == "minutes") { if ($value >59) { $hour = intval($value/60); - $value = $value%60; + $mins = $value%60; } } elseif ($modifier == "hours") { @@ -1092,6 +1093,14 @@ SQL; ->setDbValue($p_criteriaData['etc']['sp_limit_value']) ->setDbBlockId($this->id) ->save(); + + // insert repeate track option + $qry = new CcBlockcriteria(); + $qry->setDbCriteria("repeat_tracks") + ->setDbModifier("N/A") + ->setDbValue($p_criteriaData['etc']['sp_repeat_tracks']) + ->setDbBlockId($this->id) + ->save(); } /** @@ -1104,7 +1113,12 @@ SQL; $this->saveSmartBlockCriteria($p_criteria); $insertList = $this->getListOfFilesUnderLimit(); $this->deleteAllFilesFromBlock(); - $this->addAudioClips(array_keys($insertList)); + // constrcut id array + $ids = array(); + foreach ($insertList as $ele) { + $ids[] = $ele['id']; + } + $this->addAudioClips(array_values($ids)); // update length in playlist contents. $this->updateBlockLengthInAllPlaylist(); @@ -1133,6 +1147,7 @@ SQL; $info = $this->getListofFilesMeetCriteria(); $files = $info['files']; $limit = $info['limit']; + $repeat = $info['repeat_tracks']; $insertList = array(); $totalTime = 0; @@ -1141,19 +1156,37 @@ SQL; // this moves the pointer to the first element in the collection $files->getFirst(); $iterator = $files->getIterator(); - while ($iterator->valid() && $totalTime < $limit['time']) { + + $isBlockFull = false; + + while ($iterator->valid()) { $id = $iterator->current()->getDbId(); $length = Application_Common_DateHelper::calculateLengthInSeconds($iterator->current()->getDbLength()); - $insertList[$id] = $length; + $insertList[] = array('id'=>$id, 'length'=>$length); $totalTime += $length; $totalItems++; - - if ((!is_null($limit['items']) && $limit['items'] == count($insertList)) || $totalItems > 500) { + + if ((!is_null($limit['items']) && $limit['items'] == count($insertList)) || $totalItems > 500 || $totalTime > $limit['time']) { + $isBlockFull = true; break; } $iterator->next(); } + + $sizeOfInsert = count($insertList); + + // if block is not full and reapeat_track is check, fill up more + while (!$isBlockFull && $repeat == 1) { + $randomEleKey = array_rand(array_slice($insertList, 0, $sizeOfInsert)); + $insertList[] = $insertList[$randomEleKey]; + $totalTime += $insertList[$randomEleKey]['length']; + $totalItems++; + + if ((!is_null($limit['items']) && $limit['items'] == count($insertList)) || $totalItems > 500 || $totalTime > $limit['time']) { + break; + } + } return $insertList; } @@ -1201,6 +1234,8 @@ SQL; if ($criteria == "limit") { $storedCrit["limit"] = array("value"=>$value, "modifier"=>$modifier); + } else if($criteria == "repeat_tracks") { + $storedCrit["repeat_tracks"] = array("value"=>$value); } else { $storedCrit["crit"][$criteria][] = array("criteria"=>$criteria, "value"=>$value, "modifier"=>$modifier, "extra"=>$extra, "display_name"=>$criteriaOptions[$criteria]); } @@ -1316,6 +1351,7 @@ SQL; } // construct limit restriction $limits = array(); + if (isset($storedCrit['limit'])) { if ($storedCrit['limit']['modifier'] == "items") { $limits['time'] = 1440 * 60; @@ -1327,10 +1363,16 @@ SQL; $limits['items'] = null; } } + + $repeatTracks = 0; + if (isset($storedCrit['repeat_tracks'])) { + $repeatTracks = $storedCrit['repeat_tracks']['value']; + } + try { $out = $qry->setFormatter(ModelCriteria::FORMAT_ON_DEMAND)->find(); - return array("files"=>$out, "limit"=>$limits, "count"=>$out->count()); + return array("files"=>$out, "limit"=>$limits, "repeat_tracks"=> $repeatTracks, "count"=>$out->count()); } catch (Exception $e) { Logging::info($e); } @@ -1376,7 +1418,7 @@ SQL; $output['etc'][$ele['name']] = $ele['value']; } } - + return $output; } // smart block functions end diff --git a/airtime_mvc/application/models/Datatables.php b/airtime_mvc/application/models/Datatables.php index 65e7b1256..7545aec71 100644 --- a/airtime_mvc/application/models/Datatables.php +++ b/airtime_mvc/application/models/Datatables.php @@ -13,9 +13,9 @@ class Application_Model_Datatables if ($dbname == 'utime' || $dbname == 'mtime') { $input1 = isset($info[0])?Application_Common_DateHelper::ConvertToUtcDateTimeString($info[0]):null; $input2 = isset($info[1])?Application_Common_DateHelper::ConvertToUtcDateTimeString($info[1]):null; - } else if($dbname == 'bit_rate') { - $input1 = isset($info[0])?intval($info[0]) * 1000:null; - $input2 = isset($info[1])?intval($info[1]) * 1000:null; + } else if($dbname == 'bit_rate' || $dbname == 'sample_rate') { + $input1 = isset($info[0])?doubleval($info[0]) * 1000:null; + $input2 = isset($info[1])?doubleval($info[1]) * 1000:null; } else { $input1 = isset($info[0])?$info[0]:null; $input2 = isset($info[1])?$info[1]:null; diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index e65151ce4..115ea4aa0 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -189,8 +189,8 @@ class Application_Model_Preference $fade = self::getValue("default_fade"); if ($fade === "") { - // the default value of the fade is 00.500000 - return "00.500000"; + // the default value of the fade is 00.5 + return "00.5"; } // we need this function to work with 2.0 version on default_fade value in cc_pref @@ -204,9 +204,9 @@ class Application_Model_Preference $fade = $out; } - $fade = number_format($fade, 6); + $fade = number_format($fade, 2); //fades need 2 leading zeros for DateTime conversion - $fade = str_pad($fade, 9, "0", STR_PAD_LEFT); + $fade = rtrim(str_pad($fade, 5, "0", STR_PAD_LEFT), "0"); return $fade; } diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index b668f13e5..3a03a4369 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -268,7 +268,7 @@ SQL; //We need to search 24 hours before and after the show times so that that we - //capture all of the show's contents. + //capture all of the show's contents. $p_track_start= $p_start->sub(new DateInterval("PT24H"))->format("Y-m-d H:i:s"); $p_track_end = $p_end->add(new DateInterval("PT24H"))->format("Y-m-d H:i:s"); @@ -292,7 +292,8 @@ SQL; ft.artist_name AS file_artist_name, ft.album_title AS file_album_title, ft.length AS file_length, - ft.file_exists AS file_exists + ft.file_exists AS file_exists, + ft.mime AS file_mime SQL; $filesJoin = <<checkUserPermissions && $this->user->canSchedule($show->getDbId()) === false) { throw new Exception("You are not allowed to schedule show {$show->getDbName()}."); } + + if ($instance->getDbRecord()) { + throw new Exception("You cannot add files to recording shows."); + } $showEndEpoch = floatval($instance->getDbEnds("U.u")); @@ -366,12 +370,11 @@ class Application_Model_Scheduler * @param array $fileIds * @param array $playlistIds */ - private function insertAfter($scheduleItems, $schedFiles, $adjustSched = true) + private function insertAfter($scheduleItems, $schedFiles, $adjustSched = true, $mediaItems = null) { try { - $affectedShowInstances = array(); - + //dont want to recalculate times for moved items. $excludeIds = array(); foreach ($schedFiles as $file) { @@ -384,7 +387,17 @@ class Application_Model_Scheduler foreach ($scheduleItems as $schedule) { $id = intval($schedule["id"]); - + + // if mediaItmes is passed in, we want to create contents + // at the time of insert. This is for dyanmic blocks or + // playlist that contains dynamic blocks + if ($mediaItems != null) { + $schedFiles = array(); + foreach ($mediaItems as $media) { + $schedFiles = array_merge($schedFiles, $this->retrieveMediaFiles($media["id"], $media["type"])); + } + } + if ($id !== 0) { $schedItem = CcScheduleQuery::create()->findPK($id, $this->con); $instance = $schedItem->getCcShowInstances($this->con); @@ -527,10 +540,32 @@ class Application_Model_Scheduler $this->validateRequest($scheduleItems); + $requireDynamicContentCreation = false; + foreach ($mediaItems as $media) { - $schedFiles = array_merge($schedFiles, $this->retrieveMediaFiles($media["id"], $media["type"])); + if ($media['type'] == "playlist") { + $pl = new Application_Model_Playlist($media['id']); + if ($pl->hasDynamicBlock()) { + $requireDynamicContentCreation = true; + break; + } + } else if ($media['type'] == "block") { + $bl = new Application_Model_Block($media['id']); + if (!$bl->isStatic()) { + $requireDynamicContentCreation = true; + break; + } + } + } + + if ($requireDynamicContentCreation) { + $this->insertAfter($scheduleItems, $schedFiles, $adjustSched, $mediaItems); + } else { + foreach ($mediaItems as $media) { + $schedFiles = array_merge($schedFiles, $this->retrieveMediaFiles($media["id"], $media["type"])); + } + $this->insertAfter($scheduleItems, $schedFiles, $adjustSched); } - $this->insertAfter($scheduleItems, $schedFiles, $adjustSched); $this->con->commit(); diff --git a/airtime_mvc/application/models/Show.php b/airtime_mvc/application/models/Show.php index 41ffbeefd..6f088046d 100644 --- a/airtime_mvc/application/models/Show.php +++ b/airtime_mvc/application/models/Show.php @@ -270,6 +270,13 @@ SQL; try { //update the status flag in cc_schedule. + + /* Since we didn't use a propel object when updating + * cc_show_instances table we need to clear the instances + * so the correct information is retrieved from the db + */ + CcShowInstancesPeer::clearInstancePool(); + $instances = CcShowInstancesQuery::create() ->filterByDbEnds($current_timestamp, Criteria::GREATER_THAN) ->filterByDbShowId($this->_showId) @@ -1253,6 +1260,7 @@ SQL; if ($data['add_show_id'] != -1) { $con = Propel::getConnection(CcSchedulePeer::DATABASE_NAME); $con->beginTransaction(); + //current timesamp in UTC. $current_timestamp = gmdate("Y-m-d H:i:s"); diff --git a/airtime_mvc/application/models/ShowBuilder.php b/airtime_mvc/application/models/ShowBuilder.php index 8436c3011..789d5990a 100644 --- a/airtime_mvc/application/models/ShowBuilder.php +++ b/airtime_mvc/application/models/ShowBuilder.php @@ -41,6 +41,7 @@ class Application_Model_ShowBuilder "fadein" => "", "fadeout" => "", "image" => false, + "mime" => null, "color" => "", //in hex without the '#' sign. "backgroundColor" => "", //in hex without the '#' sign. ); @@ -277,6 +278,7 @@ class Application_Model_ShowBuilder $row["cueout"] = $p_item["cue_out"]; $row["fadein"] = round(substr($p_item["fade_in"], 6), 6); $row["fadeout"] = round(substr($p_item["fade_out"], 6), 6); + $row["mime"] = $p_item["file_mime"]; $row["pos"] = $this->pos++; @@ -471,7 +473,7 @@ class Application_Model_ShowBuilder $display_items[] = $this->makeFooterRow($scheduled_items[ count($scheduled_items)-1]); } - + return array( "schedule" => $display_items, "showInstances" => $this->showInstances); diff --git a/airtime_mvc/application/models/Soundcloud.php b/airtime_mvc/application/models/Soundcloud.php index 2b1068e57..70148c05c 100644 --- a/airtime_mvc/application/models/Soundcloud.php +++ b/airtime_mvc/application/models/Soundcloud.php @@ -25,66 +25,67 @@ class Application_Model_Soundcloud public function uploadTrack($filepath, $filename, $description, $tags=array(), $release=null, $genre=null) - { - if ($this->getToken()) { - if (count($tags)) { - $tags = join(" ", $tags); - $tags = $tags." ".Application_Model_Preference::GetSoundCloudTags(); - } else { - $tags = Application_Model_Preference::GetSoundCloudTags(); - } + { - $downloadable = Application_Model_Preference::GetSoundCloudDownloadbleOption() == '1'; - - $track_data = array( - 'track[sharing]' => 'private', - 'track[title]' => $filename, - 'track[asset_data]' => '@' . $filepath, - 'track[tag_list]' => $tags, - 'track[description]' => $description, - 'track[downloadable]' => $downloadable, - - ); - - if (isset($release)) { - $release = str_replace(" ", "-", $release); - $release = str_replace(":", "-", $release); - - //YYYY-MM-DD-HH-mm-SS - $release = explode("-", $release); - $track_data['track[release_year]'] = $release[0]; - $track_data['track[release_month]'] = $release[1]; - $track_data['track[release_day]'] = $release[2]; - } - - if (isset($genre) && $genre != "") { - $track_data['track[genre]'] = $genre; - } else { - $default_genre = Application_Model_Preference::GetSoundCloudGenre(); - if ($default_genre != "") { - $track_data['track[genre]'] = $default_genre; - } - } - - $track_type = Application_Model_Preference::GetSoundCloudTrackType(); - if ($track_type != "") { - $track_data['track[track_type]'] = $track_type; - } - - $license = Application_Model_Preference::GetSoundCloudLicense(); - if ($license != "") { - $track_data['track[license]'] = $license; - } - - $response = json_decode( - $this->_soundcloud->post('tracks', $track_data), - true - ); - - return $response; - } else { + if (!$this->getToken()) { throw new NoSoundCloundToken(); + } + if (count($tags)) { + $tags = join(" ", $tags); + $tags = $tags." ".Application_Model_Preference::GetSoundCloudTags(); + } else { + $tags = Application_Model_Preference::GetSoundCloudTags(); } + + $downloadable = Application_Model_Preference::GetSoundCloudDownloadbleOption() == '1'; + + $track_data = array( + 'track[sharing]' => 'private', + 'track[title]' => $filename, + 'track[asset_data]' => '@' . $filepath, + 'track[tag_list]' => $tags, + 'track[description]' => $description, + 'track[downloadable]' => $downloadable, + + ); + + if (isset($release)) { + $release = str_replace(" ", "-", $release); + $release = str_replace(":", "-", $release); + + //YYYY-MM-DD-HH-mm-SS + $release = explode("-", $release); + $track_data['track[release_year]'] = $release[0]; + $track_data['track[release_month]'] = $release[1]; + $track_data['track[release_day]'] = $release[2]; + } + + if (isset($genre) && $genre != "") { + $track_data['track[genre]'] = $genre; + } else { + $default_genre = Application_Model_Preference::GetSoundCloudGenre(); + if ($default_genre != "") { + $track_data['track[genre]'] = $default_genre; + } + } + + $track_type = Application_Model_Preference::GetSoundCloudTrackType(); + if ($track_type != "") { + $track_data['track[track_type]'] = $track_type; + } + + $license = Application_Model_Preference::GetSoundCloudLicense(); + if ($license != "") { + $track_data['track[license]'] = $license; + } + + $response = json_decode( + $this->_soundcloud->post('tracks', $track_data), + true + ); + + return $response; + } public static function uploadSoundcloud($id) diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index 2300f4eb7..51693f46d 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -335,7 +335,7 @@ SQL; * @param boolean $p_deleteFile * */ - public function delete($deleteFromPlaylist=false) + public function delete() { $filepath = $this->getFilePath(); @@ -344,6 +344,13 @@ SQL; throw new DeleteScheduledFileException(); } + $userInfo = Zend_Auth::getInstance()->getStorage()->read(); + $user = new Application_Model_User($userInfo->id); + $isAdminOrPM = $user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER)); + if (!$isAdminOrPM && $this->getFileOwnerId() != $user->getId()) { + throw new FileNoPermissionException(); + } + $music_dir = Application_Model_MusicDir::getDirByPK($this->_file->getDbDirectory()); $type = $music_dir->getType(); @@ -352,9 +359,6 @@ SQL; Application_Model_RabbitMq::SendMessageToMediaMonitor("file_delete", $data); } - if ($deleteFromPlaylist) { - Application_Model_Playlist::DeleteFileFromAllPlaylists($this->getId()); - } // set file_exists falg to false $this->_file->setDbFileExists(false); $this->_file->save(); @@ -1161,6 +1165,10 @@ SQL; return $this->_file->getDbFileExists(); } + public function getFileOwnerId() + { + return $this->_file->getDbOwnerId(); + } // note: never call this method from controllers because it does a sleep public function uploadToSoundCloud() @@ -1209,3 +1217,4 @@ SQL; class DeleteScheduledFileException extends Exception {} class FileDoesNotExistException extends Exception {} +class FileNoPermissionException extends Exception {} diff --git a/airtime_mvc/application/models/Webstream.php b/airtime_mvc/application/models/Webstream.php index 45d3caf9a..ef3f32605 100644 --- a/airtime_mvc/application/models/Webstream.php +++ b/airtime_mvc/application/models/Webstream.php @@ -92,7 +92,7 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable if (count($leftOver) == 0) { CcWebstreamQuery::create()->findPKs($p_ids)->delete(); } else { - throw new Exception("Invalid user permissions"); + throw new WebstreamNoPermissionException; } } @@ -185,7 +185,7 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable } $mediaUrl = self::getMediaUrl($url, $mime, $content_length_found); - if (preg_match("/(x-mpegurl)|(xspf\+xml)|(pls\+xml)/", $mime)) { + if (preg_match("/(x-mpegurl)|(xspf\+xml)|(pls\+xml)|(x-scpls)/", $mime)) { list($mime, $content_length_found) = self::discoverStreamMime($mediaUrl); } } catch (Exception $e) { @@ -307,7 +307,7 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable $media_url = self::getM3uUrl($url); } elseif (preg_match("/xspf\+xml/", $mime)) { $media_url = self::getXspfUrl($url); - } elseif (preg_match("/pls\+xml/", $mime)) { + } elseif (preg_match("/pls\+xml/", $mime) || preg_match("/x-scpls/", $mime)) { $media_url = self::getPlsUrl($url); } elseif (preg_match("/(mpeg|ogg)/", $mime)) { if ($content_length_found) { @@ -370,3 +370,6 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable return $webstream->getDbId(); } } + +class WebstreamNoPermissionException extends Exception {} + diff --git a/airtime_mvc/application/models/airtime/map/CcBlockTableMap.php b/airtime_mvc/application/models/airtime/map/CcBlockTableMap.php index 4495c9b1d..0ce86613c 100644 --- a/airtime_mvc/application/models/airtime/map/CcBlockTableMap.php +++ b/airtime_mvc/application/models/airtime/map/CcBlockTableMap.php @@ -54,7 +54,7 @@ class CcBlockTableMap extends TableMap { */ public function buildRelations() { - $this->addRelation('CcSubjs', 'CcSubjs', RelationMap::MANY_TO_ONE, array('creator_id' => 'id', ), null, null); + $this->addRelation('CcSubjs', 'CcSubjs', RelationMap::MANY_TO_ONE, array('creator_id' => 'id', ), 'CASCADE', null); $this->addRelation('CcPlaylistcontents', 'CcPlaylistcontents', RelationMap::ONE_TO_MANY, array('id' => 'block_id', ), 'CASCADE', null); $this->addRelation('CcBlockcontents', 'CcBlockcontents', RelationMap::ONE_TO_MANY, array('id' => 'block_id', ), 'CASCADE', null); $this->addRelation('CcBlockcriteria', 'CcBlockcriteria', RelationMap::ONE_TO_MANY, array('id' => 'block_id', ), 'CASCADE', null); diff --git a/airtime_mvc/application/models/airtime/map/CcSubjsTableMap.php b/airtime_mvc/application/models/airtime/map/CcSubjsTableMap.php index ffbe8cded..d4f83529a 100644 --- a/airtime_mvc/application/models/airtime/map/CcSubjsTableMap.php +++ b/airtime_mvc/application/models/airtime/map/CcSubjsTableMap.php @@ -64,7 +64,7 @@ class CcSubjsTableMap extends TableMap { $this->addRelation('CcPerms', 'CcPerms', RelationMap::ONE_TO_MANY, array('id' => 'subj', ), 'CASCADE', null); $this->addRelation('CcShowHosts', 'CcShowHosts', RelationMap::ONE_TO_MANY, array('id' => 'subjs_id', ), 'CASCADE', null); $this->addRelation('CcPlaylist', 'CcPlaylist', RelationMap::ONE_TO_MANY, array('id' => 'creator_id', ), 'CASCADE', null); - $this->addRelation('CcBlock', 'CcBlock', RelationMap::ONE_TO_MANY, array('id' => 'creator_id', ), null, null); + $this->addRelation('CcBlock', 'CcBlock', RelationMap::ONE_TO_MANY, array('id' => 'creator_id', ), 'CASCADE', null); $this->addRelation('CcPref', 'CcPref', RelationMap::ONE_TO_MANY, array('id' => 'subjid', ), 'CASCADE', null); $this->addRelation('CcSess', 'CcSess', RelationMap::ONE_TO_MANY, array('id' => 'userid', ), 'CASCADE', null); $this->addRelation('CcSubjsToken', 'CcSubjsToken', RelationMap::ONE_TO_MANY, array('id' => 'user_id', ), 'CASCADE', null); diff --git a/airtime_mvc/application/models/airtime/om/BaseCcSubjsPeer.php b/airtime_mvc/application/models/airtime/om/BaseCcSubjsPeer.php index dbd9978d7..ad60edbb4 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcSubjsPeer.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcSubjsPeer.php @@ -407,6 +407,9 @@ abstract class BaseCcSubjsPeer { // Invalidate objects in CcPlaylistPeer instance pool, // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule. CcPlaylistPeer::clearInstancePool(); + // Invalidate objects in CcBlockPeer instance pool, + // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule. + CcBlockPeer::clearInstancePool(); // Invalidate objects in CcPrefPeer instance pool, // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule. CcPrefPeer::clearInstancePool(); diff --git a/airtime_mvc/application/views/scripts/api/status.phtml b/airtime_mvc/application/views/scripts/api/status.phtml index 1cffa8b84..b597b4b71 100644 --- a/airtime_mvc/application/views/scripts/api/status.phtml +++ b/airtime_mvc/application/views/scripts/api/status.phtml @@ -1,3 +1,3 @@ - -
-
-
-
- Help Airtime improve by letting us know how you are using it. This info - will be collected regularly in order to enhance your user experience. -

- Click "Yes, help Airtime" and we'll make sure the features you use are - constantly improving. -
-
- - element->getElement('SupportFeedback')->hasErrors()) : ?> -
    - element->getElement('SupportFeedback')->getMessages() as $error): ?> -
  • - -
- -
-
- Click the box below to advertise your station on - Sourcefabric.org. - In order to promote your station, "Send support feedback" must be enabled. This data will be collected in addition to the support feedback. -
-
- - element->getElement('Publicise')->hasErrors()) : ?> -
    - element->getElement('Publicise')->getMessages() as $error): ?> -
  • - -
- -
-
- -
- -
-
- Show me what I am sending -
- element->getElement('SendInfo') ?> -
-
-
-
-
- privacyChecked){?> - - - Terms and Conditions - -
-
- + diff --git a/airtime_mvc/application/views/scripts/form/smart-block-criteria.phtml b/airtime_mvc/application/views/scripts/form/smart-block-criteria.phtml index 0f27248ca..3c5ab4643 100644 --- a/airtime_mvc/application/views/scripts/form/smart-block-criteria.phtml +++ b/airtime_mvc/application/views/scripts/form/smart-block-criteria.phtml @@ -30,8 +30,22 @@
criteriasLength; $i++) {?> - modRowMap[$i]; $j++) {?> -
0) && ($this->element->getElement("sp_criteria_field_".$i."_".$j)->getAttrib('disabled') == 'disabled')) { + modRowMap[$i]; $j++) { + if ($this->modRowMap[$i] > 1 && $j != $this->modRowMap[$i]-1) $logicLabel = 'or'; + else $logicLabel = 'and'; + $disabled = $this->element->getElement("sp_criteria_field_".$i."_".$j)->getAttrib('disabled') == 'disabled'?true:false; + // determine if the next row is disabled and only display the logic label if it isn't + if ($j == $this->modRowMap[$i]-1 && $i < 25) { + $n = $i+1; + $nextIndex = $n."_0"; + } elseif ($j+1 < $this->modRowMap[$i]-1) { + $n = $j+1; + $nextIndex = $i."_".$n; + + } + $nextDisabled = $this->element->getElement("sp_criteria_field_".$nextIndex)->getAttrib('disabled') == 'disabled'?true:false; + ?> +
0) && $disabled) { echo 'style=display:none'; } ?>> element->getElement("sp_criteria_field_".$i."_".$j) ?> @@ -45,6 +59,9 @@ + > + + element->getElement("sp_criteria_field_".$i."_".$j)->hasErrors()) : ?> element->getElement("sp_criteria_field_".$i."_".$j)->getMessages() as $error): ?> @@ -59,6 +76,20 @@
+
+ element->getElement('sp_repeat_tracks')->getLabel() ?> + + element->getElement('sp_repeat_tracks')?> + element->getElement("sp_repeat_tracks")->hasErrors()) : ?> + element->getElement("sp_repeat_tracks")->getMessages() as $error): ?> + + + + + +
+
+
element->getElement('sp_limit_value')->getLabel() ?> element->getElement('sp_limit_value')?> diff --git a/airtime_mvc/application/views/scripts/library/edit-file-md.phtml b/airtime_mvc/application/views/scripts/library/edit-file-md.phtml index 6b7696c0a..bdead9832 100644 --- a/airtime_mvc/application/views/scripts/library/edit-file-md.phtml +++ b/airtime_mvc/application/views/scripts/library/edit-file-md.phtml @@ -1,6 +1,6 @@

Edit Metadata

- form->setAction($this->url()); + form->setAction($this->url()); echo $this->form; ?>
diff --git a/airtime_mvc/application/views/scripts/library/library.phtml b/airtime_mvc/application/views/scripts/library/library.phtml index 0b60020c5..ec96a3a44 100644 --- a/airtime_mvc/application/views/scripts/library/library.phtml +++ b/airtime_mvc/application/views/scripts/library/library.phtml @@ -1,5 +1,5 @@ -
+
Advanced Search Options
diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml index 70176ff9e..87cd0662f 100644 --- a/airtime_mvc/application/views/scripts/playlist/update.phtml +++ b/airtime_mvc/application/views/scripts/playlist/update.phtml @@ -92,5 +92,13 @@ if ($item['type'] == 2) { -
  • Empty playlist
  • +
  • +obj instanceof Application_Model_Block) { + echo 'Empty smart block'; + } else { + echo 'Empty playlist'; + } +?> +
  • diff --git a/airtime_mvc/application/views/scripts/schedule/show-list.phtml b/airtime_mvc/application/views/scripts/schedule/show-list.phtml index 992701db3..939ab6afd 100644 --- a/airtime_mvc/application/views/scripts/schedule/show-list.phtml +++ b/airtime_mvc/application/views/scripts/schedule/show-list.phtml @@ -1,2 +1,2 @@ -
    +
    \ No newline at end of file diff --git a/airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml b/airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml index 92331d6cc..46ce9488e 100644 --- a/airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml +++ b/airtime_mvc/application/views/scripts/showbuilder/builderDialog.phtml @@ -1,7 +1,7 @@
    -
    +
    Advanced Search Options
    diff --git a/airtime_mvc/application/views/scripts/webstream/webstream.phtml b/airtime_mvc/application/views/scripts/webstream/webstream.phtml index 1757f9be0..6695c82fc 100644 --- a/airtime_mvc/application/views/scripts/webstream/webstream.phtml +++ b/airtime_mvc/application/views/scripts/webstream/webstream.phtml @@ -9,6 +9,10 @@
  • New Webstream
  • + +
    + +
    obj)) : ?>
    @@ -37,24 +41,23 @@
    - - -
    -
    - -
    - -
    -
    - -
    -
    - -
    +
    - - + +
    + + +
    +
    + +
    + +
    +
    + +
    +
    No webstream
    diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml index fc3e6a51f..1a69020d3 100644 --- a/airtime_mvc/build/schema.xml +++ b/airtime_mvc/build/schema.xml @@ -250,7 +250,7 @@ - + diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql index d7e1107a4..d30faa09d 100644 --- a/airtime_mvc/build/sql/schema.sql +++ b/airtime_mvc/build/sql/schema.sql @@ -697,7 +697,7 @@ ALTER TABLE "cc_playlistcontents" ADD CONSTRAINT "cc_playlistcontents_block_id_f ALTER TABLE "cc_playlistcontents" ADD CONSTRAINT "cc_playlistcontents_playlist_id_fkey" FOREIGN KEY ("playlist_id") REFERENCES "cc_playlist" ("id") ON DELETE CASCADE; -ALTER TABLE "cc_block" ADD CONSTRAINT "cc_block_createdby_fkey" FOREIGN KEY ("creator_id") REFERENCES "cc_subjs" ("id"); +ALTER TABLE "cc_block" ADD CONSTRAINT "cc_block_createdby_fkey" FOREIGN KEY ("creator_id") REFERENCES "cc_subjs" ("id") ON DELETE CASCADE; ALTER TABLE "cc_blockcontents" ADD CONSTRAINT "cc_blockcontents_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "cc_files" ("id") ON DELETE CASCADE; diff --git a/airtime_mvc/public/css/add-show.css b/airtime_mvc/public/css/add-show.css index 0dd479a3c..4ca5dc469 100644 --- a/airtime_mvc/public/css/add-show.css +++ b/airtime_mvc/public/css/add-show.css @@ -1,129 +1,129 @@ -#schedule-add-show, -#fullcalendar_show_display { - float: left; -} - -#schedule-add-show { - font-size: 12px; - /*width: 25%;*/ - width:310px; -} - -#schedule-add-show textarea { - width: 99%; - height: 80px; -} - -#fullcalendar_show_display { - width: 60%; -} - - - -#schedule-add-show .ui-tabs-panel { - padding-top: 8px; -} -#schedule-add-show fieldset { - padding:8px; - margin-bottom:8px; -} - -#schedule-add-show dl { - padding:8px; - margin-bottom:8px; - margin:0; - padding:0; - width:100%; -} -#schedule-add-show dd { - padding: 4px 0; - float: left; - font-size: 1.2em; - margin: 0; - padding: 4px 0 4px 15px; -} -#schedule-add-show dt, #schedule-add-show dt.big { - clear: left; - color: #666666; - float: left; - font-size: 1.2em; - font-weight: bold; - margin: 0; - padding: 4px 0; - text-align: left; - min-width:103px; - clear:left; -} -#schedule-add-show dt.big { - min-width:130px; -} -#schedule-add-show dt.block-display, #schedule-add-show dd.block-display { - display:block; - float:none; - margin-left:0; - padding-left:0; -} - -#schedule-add-show dt label { - padding-right:0; -} -.wrapp-label { - padding:0; - height:16px; - display:block; - line-height:18px; -} -label.wrapp-label input[type="checkbox"] { - float:left; - margin:-1px 2px 0 0; -} - -#schedule-add-show fieldset:last-child { - margin-bottom:0; -} -#schedule-add-show fieldset dd input[type="checkbox"] { - margin-top:2px; -} -#add_show_day_check-element.block-display { - margin-bottom:15px; - margin-top:7px; -} -#add_show_day_check-element.block-display label.wrapp-label { - font-size:12px; - float:left; - margin-right:10px; -} -#add_show_name-element .input_text { - /*width:99%;*/ -} - -#schedule-add-show-overlap { - border: 1px solid #c83f3f; - background: #c6b4b4; - margin-top:8px; - padding:8px; - color:#902d2d; - display:none; -} - -#add_show_hosts-element { - max-height: 80px; - min-width: 150px; - overflow: auto; -} - -#add_show_start_time, #add_show_end_time { - width: 54px; - margin-left:10px; -} - -#add_show_end_date_no_repeat, #add_show_start_date { - width: 89px; - -} - -#add_show_duration { - background: #AAAAAA; - cursor: default; - width: 65px; -} +#schedule-add-show, +#fullcalendar_show_display { + float: left; +} + +#schedule-add-show { + font-size: 12px; + /*width: 25%;*/ + width:310px; +} + +#schedule-add-show textarea { + width: 99%; + height: 80px; +} + +#fullcalendar_show_display { + width: 60%; +} + + + +#schedule-add-show .ui-tabs-panel { + padding-top: 8px; +} +#schedule-add-show fieldset { + padding:8px; + margin-bottom:8px; +} + +#schedule-add-show dl { + padding:8px; + margin-bottom:8px; + margin:0; + padding:0; + width:100%; +} +#schedule-add-show dd { + padding: 4px 0; + float: left; + font-size: 1.2em; + margin: 0; + padding: 4px 0 4px 15px; +} +#schedule-add-show dt, #schedule-add-show dt.big { + clear: left; + color: #666666; + float: left; + font-size: 1.2em; + font-weight: bold; + margin: 0; + padding: 4px 0; + text-align: left; + min-width:103px; + clear:left; +} +#schedule-add-show dt.big { + min-width:130px; +} +#schedule-add-show dt.block-display, #schedule-add-show dd.block-display { + display:block; + float:none; + margin-left:0; + padding-left:0; +} + +#schedule-add-show dt label { + padding-right:0; +} +.wrapp-label { + padding:0; + height:16px; + display:block; + line-height:18px; +} +label.wrapp-label input[type="checkbox"] { + float:left; + margin:-1px 2px 0 0; +} + +#schedule-add-show fieldset:last-child { + margin-bottom:0; +} +#schedule-add-show fieldset dd input[type="checkbox"] { + margin-top:2px; +} +#add_show_day_check-element.block-display { + margin-bottom:15px; + margin-top:7px; +} +#add_show_day_check-element.block-display label.wrapp-label { + font-size:12px; + float:left; + margin-right:10px; +} +#add_show_name-element .input_text { + /*width:99%;*/ +} + +#schedule-add-show-overlap { + border: 1px solid #c83f3f; + background: #c6b4b4; + margin-top:8px; + padding:8px; + color:#902d2d; + display:none; +} + +#add_show_hosts-element { + max-height: 80px; + min-width: 150px; + overflow: auto; +} + +#add_show_start_time, #add_show_end_time { + width: 54px; + margin-left:10px; +} + +#add_show_end_date_no_repeat, #add_show_start_date { + width: 89px; + +} + +#add_show_duration { + background: #AAAAAA; + cursor: default; + width: 65px; +} diff --git a/airtime_mvc/public/css/colorpicker/css/colorpicker.css b/airtime_mvc/public/css/colorpicker/css/colorpicker.css index d704c4c64..9b449e780 100644 --- a/airtime_mvc/public/css/colorpicker/css/colorpicker.css +++ b/airtime_mvc/public/css/colorpicker/css/colorpicker.css @@ -1,162 +1,162 @@ -.colorpicker { - width: 356px; - height: 176px; - overflow: hidden; - position: absolute; - background: url(../images/colorpicker_background.png); - font-family: Arial, Helvetica, sans-serif; - display: none; - z-index: 1003; /* so it can display above the jQuery dialog*/ -} -.colorpicker_color { - width: 150px; - height: 150px; - left: 14px; - top: 13px; - position: absolute; - background: #f00; - overflow: hidden; - cursor: crosshair; -} -.colorpicker_color div { - position: absolute; - top: 0; - left: 0; - width: 150px; - height: 150px; - background: url(../images/colorpicker_overlay.png); -} -.colorpicker_color div div { - position: absolute; - top: 0; - left: 0; - width: 11px; - height: 11px; - overflow: hidden; - background: url(../images/colorpicker_select.gif); - margin: -5px 0 0 -5px; -} -.colorpicker_hue { - position: absolute; - top: 13px; - left: 171px; - width: 35px; - height: 150px; - cursor: n-resize; -} -.colorpicker_hue div { - position: absolute; - width: 35px; - height: 9px; - overflow: hidden; - background: url(../images/colorpicker_indic.gif) left top; - margin: -4px 0 0 0; - left: 0px; -} -.colorpicker_new_color { - position: absolute; - width: 60px; - height: 30px; - left: 213px; - top: 13px; - background: #f00; -} -.colorpicker_current_color { - position: absolute; - width: 60px; - height: 30px; - left: 283px; - top: 13px; - background: #f00; -} -.colorpicker input { - background-color: transparent; - border: 1px solid transparent; - position: absolute; - font-size: 10px; - font-family: Arial, Helvetica, sans-serif; - color: #898989; - top: 4px; - right: 11px; - text-align: right; - margin: 0; - padding: 0; - height: 11px; -} -.colorpicker_hex { - position: absolute; - width: 72px; - height: 22px; - background: url(../images/colorpicker_hex.png) top; - left: 212px; - top: 142px; -} -.colorpicker_hex input { - right: 6px; -} -.colorpicker_field { - height: 22px; - width: 62px; - background-position: top; - position: absolute; -} -.colorpicker_field span { - position: absolute; - width: 12px; - height: 22px; - overflow: hidden; - top: 0; - right: 0; - cursor: n-resize; -} -.colorpicker_rgb_r { - background-image: url(../images/colorpicker_rgb_r.png); - top: 52px; - left: 212px; -} -.colorpicker_rgb_g { - background-image: url(../images/colorpicker_rgb_g.png); - top: 82px; - left: 212px; -} -.colorpicker_rgb_b { - background-image: url(../images/colorpicker_rgb_b.png); - top: 112px; - left: 212px; -} -.colorpicker_hsb_h { - background-image: url(../images/colorpicker_hsb_h.png); - top: 52px; - left: 282px; -} -.colorpicker_hsb_s { - background-image: url(../images/colorpicker_hsb_s.png); - top: 82px; - left: 282px; -} -.colorpicker_hsb_b { - background-image: url(../images/colorpicker_hsb_b.png); - top: 112px; - left: 282px; -} -.colorpicker_submit { - position: absolute; - width: 22px; - height: 22px; - background: url(../images/colorpicker_submit.png) top; - left: 322px; - top: 142px; - overflow: hidden; -} -.colorpicker_focus { - background-position: center; -} -.colorpicker_hex.colorpicker_focus { - background-position: bottom; -} -.colorpicker_submit.colorpicker_focus { - background-position: bottom; -} -.colorpicker_slider { - background-position: bottom; -} +.colorpicker { + width: 356px; + height: 176px; + overflow: hidden; + position: absolute; + background: url(../images/colorpicker_background.png); + font-family: Arial, Helvetica, sans-serif; + display: none; + z-index: 1003; /* so it can display above the jQuery dialog*/ +} +.colorpicker_color { + width: 150px; + height: 150px; + left: 14px; + top: 13px; + position: absolute; + background: #f00; + overflow: hidden; + cursor: crosshair; +} +.colorpicker_color div { + position: absolute; + top: 0; + left: 0; + width: 150px; + height: 150px; + background: url(../images/colorpicker_overlay.png); +} +.colorpicker_color div div { + position: absolute; + top: 0; + left: 0; + width: 11px; + height: 11px; + overflow: hidden; + background: url(../images/colorpicker_select.gif); + margin: -5px 0 0 -5px; +} +.colorpicker_hue { + position: absolute; + top: 13px; + left: 171px; + width: 35px; + height: 150px; + cursor: n-resize; +} +.colorpicker_hue div { + position: absolute; + width: 35px; + height: 9px; + overflow: hidden; + background: url(../images/colorpicker_indic.gif) left top; + margin: -4px 0 0 0; + left: 0px; +} +.colorpicker_new_color { + position: absolute; + width: 60px; + height: 30px; + left: 213px; + top: 13px; + background: #f00; +} +.colorpicker_current_color { + position: absolute; + width: 60px; + height: 30px; + left: 283px; + top: 13px; + background: #f00; +} +.colorpicker input { + background-color: transparent; + border: 1px solid transparent; + position: absolute; + font-size: 10px; + font-family: Arial, Helvetica, sans-serif; + color: #898989; + top: 4px; + right: 11px; + text-align: right; + margin: 0; + padding: 0; + height: 11px; +} +.colorpicker_hex { + position: absolute; + width: 72px; + height: 22px; + background: url(../images/colorpicker_hex.png) top; + left: 212px; + top: 142px; +} +.colorpicker_hex input { + right: 6px; +} +.colorpicker_field { + height: 22px; + width: 62px; + background-position: top; + position: absolute; +} +.colorpicker_field span { + position: absolute; + width: 12px; + height: 22px; + overflow: hidden; + top: 0; + right: 0; + cursor: n-resize; +} +.colorpicker_rgb_r { + background-image: url(../images/colorpicker_rgb_r.png); + top: 52px; + left: 212px; +} +.colorpicker_rgb_g { + background-image: url(../images/colorpicker_rgb_g.png); + top: 82px; + left: 212px; +} +.colorpicker_rgb_b { + background-image: url(../images/colorpicker_rgb_b.png); + top: 112px; + left: 212px; +} +.colorpicker_hsb_h { + background-image: url(../images/colorpicker_hsb_h.png); + top: 52px; + left: 282px; +} +.colorpicker_hsb_s { + background-image: url(../images/colorpicker_hsb_s.png); + top: 82px; + left: 282px; +} +.colorpicker_hsb_b { + background-image: url(../images/colorpicker_hsb_b.png); + top: 112px; + left: 282px; +} +.colorpicker_submit { + position: absolute; + width: 22px; + height: 22px; + background: url(../images/colorpicker_submit.png) top; + left: 322px; + top: 142px; + overflow: hidden; +} +.colorpicker_focus { + background-position: center; +} +.colorpicker_hex.colorpicker_focus { + background-position: bottom; +} +.colorpicker_submit.colorpicker_focus { + background-position: bottom; +} +.colorpicker_slider { + background-position: bottom; +} diff --git a/airtime_mvc/public/css/jquery.ui.timepicker.css b/airtime_mvc/public/css/jquery.ui.timepicker.css index 374da6bbb..01f57a5a9 100644 --- a/airtime_mvc/public/css/jquery.ui.timepicker.css +++ b/airtime_mvc/public/css/jquery.ui.timepicker.css @@ -1,72 +1,72 @@ -/* - * Timepicker stylesheet - * Highly inspired from datepicker - * FG - Nov 2010 - Web3R - * - * version 0.0.3 : Fixed some settings, more dynamic - * version 0.0.4 : Removed width:100% on tables - * version 0.1.1 : set width 0 on tables to fix an ie6 bug - */ - -.ui-timepicker-inline { display: inline; } - -#ui-timepicker-div { - padding: 0.2em; - z-index: 1000; -} -.ui-timepicker-table { display: inline-table; width: 0; } -.ui-timepicker-table table { margin:0.15em 0 0 0; border-collapse: collapse; } - -.ui-timepicker-hours, .ui-timepicker-minutes { padding: 0.2em; } - -.ui-timepicker-table .ui-timepicker-title { line-height: 1.8em; text-align: center; } -.ui-timepicker-table td { padding: 0.1em; width: 2.2em; } -.ui-timepicker-table th.periods { padding: 0.1em; width: 2.2em; } - -/* span for disabled cells */ -.ui-timepicker-table td span { - display:block; - padding:0.2em 0.3em 0.2em 0.5em; - width: 1.2em; - - text-align:right; - text-decoration:none; -} -/* anchors for clickable cells */ -.ui-timepicker-table td a { - display:block; - padding:0.2em 0.3em 0.2em 0.5em; - width: 1.2em; - cursor: pointer; - text-align:right; - text-decoration:none; -} - - -/* buttons and button pane styling */ -.ui-timepicker .ui-timepicker-buttonpane { - background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; -} -.ui-timepicker .ui-timepicker-buttonpane button { margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } -/* The close button */ -.ui-timepicker .ui-timepicker-close { float: right } - -/* the now button */ -.ui-timepicker .ui-timepicker-now { float: left; } - -/* the deselect button */ -.ui-timepicker .ui-timepicker-deselect { float: left; } - - -/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ -.ui-timepicker-cover { - display: none; /*sorry for IE5*/ - display/**/: block; /*sorry for IE5*/ - position: absolute; /*must have*/ - z-index: -1; /*must have*/ - filter: mask(); /*must have*/ - top: -4px; /*must have*/ - left: -4px; /*must have*/ - width: 200px; /*must have*/ - height: 200px; /*must have*/ +/* + * Timepicker stylesheet + * Highly inspired from datepicker + * FG - Nov 2010 - Web3R + * + * version 0.0.3 : Fixed some settings, more dynamic + * version 0.0.4 : Removed width:100% on tables + * version 0.1.1 : set width 0 on tables to fix an ie6 bug + */ + +.ui-timepicker-inline { display: inline; } + +#ui-timepicker-div { + padding: 0.2em; + z-index: 1000; +} +.ui-timepicker-table { display: inline-table; width: 0; } +.ui-timepicker-table table { margin:0.15em 0 0 0; border-collapse: collapse; } + +.ui-timepicker-hours, .ui-timepicker-minutes { padding: 0.2em; } + +.ui-timepicker-table .ui-timepicker-title { line-height: 1.8em; text-align: center; } +.ui-timepicker-table td { padding: 0.1em; width: 2.2em; } +.ui-timepicker-table th.periods { padding: 0.1em; width: 2.2em; } + +/* span for disabled cells */ +.ui-timepicker-table td span { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + width: 1.2em; + + text-align:right; + text-decoration:none; +} +/* anchors for clickable cells */ +.ui-timepicker-table td a { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + width: 1.2em; + cursor: pointer; + text-align:right; + text-decoration:none; +} + + +/* buttons and button pane styling */ +.ui-timepicker .ui-timepicker-buttonpane { + background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; +} +.ui-timepicker .ui-timepicker-buttonpane button { margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } +/* The close button */ +.ui-timepicker .ui-timepicker-close { float: right } + +/* the now button */ +.ui-timepicker .ui-timepicker-now { float: left; } + +/* the deselect button */ +.ui-timepicker .ui-timepicker-deselect { float: left; } + + +/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ +.ui-timepicker-cover { + display: none; /*sorry for IE5*/ + display/**/: block; /*sorry for IE5*/ + position: absolute; /*must have*/ + z-index: -1; /*must have*/ + filter: mask(); /*must have*/ + top: -4px; /*must have*/ + left: -4px; /*must have*/ + width: 200px; /*must have*/ + height: 200px; /*must have*/ } \ No newline at end of file diff --git a/airtime_mvc/public/css/plupload.queue.css b/airtime_mvc/public/css/plupload.queue.css index 3766a71f7..3d5f8d042 100644 --- a/airtime_mvc/public/css/plupload.queue.css +++ b/airtime_mvc/public/css/plupload.queue.css @@ -1,176 +1,176 @@ -/* - Plupload -------------------------------------------------------------------- */ - -.plupload_button { - display: -moz-inline-box; /* FF < 3*/ - display: inline-block; - font: normal 12px sans-serif; - text-decoration: none; - color: #42454a; - border: 1px solid #bababa; - padding: 2px 8px 3px 20px; - margin-right: 4px; - background: #f3f3f3 url('img/buttons.png') no-repeat 0 center; - outline: 0; - - /* Optional rounded corners for browsers that support it */ - -moz-border-radius: 3px; - -khtml-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; -} - -.plupload_button:hover { - color: #000; - text-decoration: none; -} - -.plupload_disabled, a.plupload_disabled:hover { - color: #737373; - border-color: #c5c5c5; - background: #ededed url('img/buttons-disabled.png') no-repeat 0 center; - cursor: default; -} - -.plupload_add { - background-position: -181px center; -} - -.plupload_wrapper { - font: normal 11px Verdana,sans-serif; - width: 100%; -} - -.plupload_container { - padding: 8px; - background: url('img/transp50.png'); - /*-moz-border-radius: 5px;*/ -} - -.plupload_container input { - border: 1px solid #DDD; - font: normal 11px Verdana,sans-serif; - width: 98%; -} - -.plupload_header {background: #2A2C2E url('img/backgrounds.gif') repeat-x;} -.plupload_header_content { - min-height: 56px; - padding-left: 10px; - color: #FFF; -} -.plupload_header_title { - font: normal 18px sans-serif; - padding: 6px 0 3px; -} -.plupload_header_text { - font: normal 12px sans-serif; -} - -.plupload_filelist { - margin: 0; - padding: 0; - list-style: none; -} - -.plupload_scroll .plupload_filelist { - height: 185px; - background: #F5F5F5; - overflow-y: scroll; -} - -.plupload_filelist li { - padding: 10px 8px; - background: #F5F5F5 url('img/backgrounds.gif') repeat-x 0 -156px; - border-bottom: 1px solid #DDD; -} - -.plupload_filelist_header, .plupload_filelist_footer { - background: #DFDFDF; - padding: 8px 8px; - color: #42454A; -} -.plupload_filelist_header { - border-top: 1px solid #EEE; - border-bottom: 1px solid #CDCDCD; -} - -.plupload_filelist_footer {border-top: 1px solid #FFF; height: 22px; line-height: 20px; vertical-align: middle;} -.plupload_file_name {float: left; overflow: hidden} -.plupload_file_status {color: #777;} -.plupload_file_status span {color: #42454A;} -.plupload_file_size, .plupload_file_status, .plupload_progress { - float: right; - width: 80px; -} -.plupload_file_size, .plupload_file_status, .plupload_file_action {text-align: right;} - -.plupload_filelist .plupload_file_name {width: 68%;} - -.plupload_file_action { - float: right; - width: 16px; - height: 16px; - margin-left: 15px; -} - -.plupload_file_action * { - display: none; - width: 16px; - height: 16px; -} - -li.plupload_uploading {background: #ECF3DC url('img/backgrounds.gif') repeat-x 0 -238px;} -li.plupload_done {color:#AAA} - -li.plupload_delete a { - background: url('img/delete.gif'); -} - -li.plupload_failed a { - background: url('img/error.gif'); - cursor: default; -} - -li.plupload_done a { - background: url('img/done.gif'); - cursor: default; -} - -.plupload_progress, .plupload_upload_status { - display: none; -} - -.plupload_progress_container { - margin-top: 3px; - border: 1px solid #CCC; - background: #FFF; - padding: 1px; -} -.plupload_progress_bar { - width: 0px; - height: 7px; - background: #CDEB8B; -} - -.plupload_scroll .plupload_filelist_header .plupload_file_action, .plupload_scroll .plupload_filelist_footer .plupload_file_action { - margin-right: 17px; -} - -/* Floats */ - -.plupload_clear,.plupload_clearer {clear: both;} -.plupload_clearer, .plupload_progress_bar { - display: block; - font-size: 0; - line-height: 0; -} - -li.plupload_droptext { - background: transparent; - text-align: center; - vertical-align: middle; - border: 0; - line-height: 165px; -} +/* + Plupload +------------------------------------------------------------------- */ + +.plupload_button { + display: -moz-inline-box; /* FF < 3*/ + display: inline-block; + font: normal 12px sans-serif; + text-decoration: none; + color: #42454a; + border: 1px solid #bababa; + padding: 2px 8px 3px 20px; + margin-right: 4px; + background: #f3f3f3 url('img/buttons.png') no-repeat 0 center; + outline: 0; + + /* Optional rounded corners for browsers that support it */ + -moz-border-radius: 3px; + -khtml-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +.plupload_button:hover { + color: #000; + text-decoration: none; +} + +.plupload_disabled, a.plupload_disabled:hover { + color: #737373; + border-color: #c5c5c5; + background: #ededed url('img/buttons-disabled.png') no-repeat 0 center; + cursor: default; +} + +.plupload_add { + background-position: -181px center; +} + +.plupload_wrapper { + font: normal 11px Verdana,sans-serif; + width: 100%; +} + +.plupload_container { + padding: 8px; + background: url('img/transp50.png'); + /*-moz-border-radius: 5px;*/ +} + +.plupload_container input { + border: 1px solid #DDD; + font: normal 11px Verdana,sans-serif; + width: 98%; +} + +.plupload_header {background: #2A2C2E url('img/backgrounds.gif') repeat-x;} +.plupload_header_content { + min-height: 56px; + padding-left: 10px; + color: #FFF; +} +.plupload_header_title { + font: normal 18px sans-serif; + padding: 6px 0 3px; +} +.plupload_header_text { + font: normal 12px sans-serif; +} + +.plupload_filelist { + margin: 0; + padding: 0; + list-style: none; +} + +.plupload_scroll .plupload_filelist { + height: 185px; + background: #F5F5F5; + overflow-y: scroll; +} + +.plupload_filelist li { + padding: 10px 8px; + background: #F5F5F5 url('img/backgrounds.gif') repeat-x 0 -156px; + border-bottom: 1px solid #DDD; +} + +.plupload_filelist_header, .plupload_filelist_footer { + background: #DFDFDF; + padding: 8px 8px; + color: #42454A; +} +.plupload_filelist_header { + border-top: 1px solid #EEE; + border-bottom: 1px solid #CDCDCD; +} + +.plupload_filelist_footer {border-top: 1px solid #FFF; height: 22px; line-height: 20px; vertical-align: middle;} +.plupload_file_name {float: left; overflow: hidden} +.plupload_file_status {color: #777;} +.plupload_file_status span {color: #42454A;} +.plupload_file_size, .plupload_file_status, .plupload_progress { + float: right; + width: 80px; +} +.plupload_file_size, .plupload_file_status, .plupload_file_action {text-align: right;} + +.plupload_filelist .plupload_file_name {width: 68%;} + +.plupload_file_action { + float: right; + width: 16px; + height: 16px; + margin-left: 15px; +} + +.plupload_file_action * { + display: none; + width: 16px; + height: 16px; +} + +li.plupload_uploading {background: #ECF3DC url('img/backgrounds.gif') repeat-x 0 -238px;} +li.plupload_done {color:#AAA} + +li.plupload_delete a { + background: url('img/delete.gif'); +} + +li.plupload_failed a { + background: url('img/error.gif'); + cursor: default; +} + +li.plupload_done a { + background: url('img/done.gif'); + cursor: default; +} + +.plupload_progress, .plupload_upload_status { + display: none; +} + +.plupload_progress_container { + margin-top: 3px; + border: 1px solid #CCC; + background: #FFF; + padding: 1px; +} +.plupload_progress_bar { + width: 0px; + height: 7px; + background: #CDEB8B; +} + +.plupload_scroll .plupload_filelist_header .plupload_file_action, .plupload_scroll .plupload_filelist_footer .plupload_file_action { + margin-right: 17px; +} + +/* Floats */ + +.plupload_clear,.plupload_clearer {clear: both;} +.plupload_clearer, .plupload_progress_bar { + display: block; + font-size: 0; + line-height: 0; +} + +li.plupload_droptext { + background: transparent; + text-align: center; + vertical-align: middle; + border: 0; + line-height: 165px; +} diff --git a/airtime_mvc/public/css/pro_dropdown_3.css b/airtime_mvc/public/css/pro_dropdown_3.css index f83211876..ad31500ae 100644 --- a/airtime_mvc/public/css/pro_dropdown_3.css +++ b/airtime_mvc/public/css/pro_dropdown_3.css @@ -1,185 +1,185 @@ -/* ================================================================ -This copyright notice must be kept untouched in the stylesheet at -all times. - -The original version of this stylesheet and the associated (x)html -is available at http://www.stunicholls.com/menu/pro_dropdown_3.html -Copyright (c) 2005-2007 Stu Nicholls. All rights reserved. -This stylesheet and the associated (x)html may be modified in any -way to fit your requirements. -=================================================================== */ - -#nav { - padding:7px 0 0 6px; - margin:0; - list-style:none; - height:28px; - background:#353535; - position:relative; - z-index:500; - font-family:Arial, Helvetica, sans-serif; - border-top:1px solid #7e7e7e; - border-bottom:1px solid #242424; - -moz-box-shadow: 0 2px 5px rgba(0,0,0,.35); - -webkit-box-shadow: 0 2px 5px rgba(0,0,0,.35); - box-shadow: 0 2px 5px rgba(0,0,0,.35); -} -#nav li.top { - display:block; - float:left; - margin:0 5px 0 0; -} -#nav li a.top_link { - display:block; - float:left; - height:18px; - color:#ccc; - text-decoration:none; - font-size:11px; - text-transform:uppercase; - font-weight:bold; - padding:4px 0 0 14px; - cursor:pointer; -} -#nav li a.top_link span { - float:left; - display:block; - padding:0 14px 0 0; -} -#nav li a.top_link span.down { - float:left; - display:block; - padding:0 28px 0 0px; - background:url(images/down_arrow.png) no-repeat right 50%; - -} -#nav li:hover a.top_link, #nav li.active a.top_link, #nav li.active:hover a.top_link { - color:#fff; - background:#000; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; -} -#nav li:hover a.top_link span { - -} -#nav li:hover a.top_link span.down { - background:url(images/down_arrow.png) no-repeat right 50%; -} -/* Default list styling */ - -#nav li:hover { - position:relative; - z-index:200; -} -#nav li:hover ul.sub { - left:1px; - top:22px; - background: #202020; - padding:3px; - border:1px solid #161616; - white-space:nowrap; - width:200px; - height:auto; - z-index:300; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -} -#nav li:hover ul.sub li { - display:block; - height:22px; - position:relative; - float:left; - width:200px; - font-weight:normal; -} -#nav li:hover ul.sub li a { - display:block; - font-size:12px; - height:20px; - width:198px; - line-height:20px; - text-indent:5px; - color:#fff; - text-decoration:none; - border:1px solid #202020; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; -} -#nav li ul.sub li a.fly { - background:#202020 url(images/arrow.png) 190px 6px no-repeat; -} -#nav li:hover ul.sub li a:hover { - background:#3d3d3d; - color:#fff; - border-color:#4e4e4e; -} -#nav li:hover ul.sub li a.fly:hover { - background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; - color:#fff; -} -#nav li strong { - display:block; - font-size:12px; - height:20px; - width:198px; - line-height:20px; - margin-bottom:3px; - text-indent:6px; - color:#ff5d1a; - border-bottom:1px solid #414141; - cursor:default; - font-weight:bold; -} -#nav li:hover li:hover ul, #nav li:hover li:hover li:hover ul, #nav li:hover li:hover li:hover li:hover ul, #nav li:hover li:hover li:hover li:hover li:hover ul { - left:200px; - top:-4px; - background: #202020; - padding:3px; - border:1px solid #161616; - white-space:nowrap; - width:200px; - z-index:400; - height:auto; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; -} -#nav ul, #nav li:hover ul ul, #nav li:hover li:hover ul ul, #nav li:hover li:hover li:hover ul ul, #nav li:hover li:hover li:hover li:hover ul ul { - position:absolute; - left:-9999px; - top:-9999px; - width:0; - height:0; - margin:0; - padding:0; - list-style:none; -} -#nav li:hover li:hover a.fly, #nav li:hover li:hover li:hover a.fly, #nav li:hover li:hover li:hover li:hover a.fly, #nav li:hover li:hover li:hover li:hover li:hover a.fly { - background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; - color:#fff; - border-color:#4e4e4e; -} - - -#nav li:hover li:hover li:hover a.fly { - background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; - color:#fff; - border-color:#4e4e4e; -} -#nav li:hover li:hover li:hover li:hover li:hover a.fly { - background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; - color:#fff; - border-color:#4e4e4e; -} -#nav li:hover li:hover li a.fly, -#nav li:hover li:hover li:hover li a.fly, -#nav li:hover li:hover li:hover li:hover li a.fly { - background:#202020 url(images/arrow.png) 190px 6px no-repeat; - color:#fff; - border-color:#202020; -} - +/* ================================================================ +This copyright notice must be kept untouched in the stylesheet at +all times. + +The original version of this stylesheet and the associated (x)html +is available at http://www.stunicholls.com/menu/pro_dropdown_3.html +Copyright (c) 2005-2007 Stu Nicholls. All rights reserved. +This stylesheet and the associated (x)html may be modified in any +way to fit your requirements. +=================================================================== */ + +#nav { + padding:7px 0 0 6px; + margin:0; + list-style:none; + height:28px; + background:#353535; + position:relative; + z-index:500; + font-family:Arial, Helvetica, sans-serif; + border-top:1px solid #7e7e7e; + border-bottom:1px solid #242424; + -moz-box-shadow: 0 2px 5px rgba(0,0,0,.35); + -webkit-box-shadow: 0 2px 5px rgba(0,0,0,.35); + box-shadow: 0 2px 5px rgba(0,0,0,.35); +} +#nav li.top { + display:block; + float:left; + margin:0 5px 0 0; +} +#nav li a.top_link { + display:block; + float:left; + height:18px; + color:#ccc; + text-decoration:none; + font-size:11px; + text-transform:uppercase; + font-weight:bold; + padding:4px 0 0 14px; + cursor:pointer; +} +#nav li a.top_link span { + float:left; + display:block; + padding:0 14px 0 0; +} +#nav li a.top_link span.down { + float:left; + display:block; + padding:0 28px 0 0px; + background:url(images/down_arrow.png) no-repeat right 50%; + +} +#nav li:hover a.top_link, #nav li.active a.top_link, #nav li.active:hover a.top_link { + color:#fff; + background:#000; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} +#nav li:hover a.top_link span { + +} +#nav li:hover a.top_link span.down { + background:url(images/down_arrow.png) no-repeat right 50%; +} +/* Default list styling */ + +#nav li:hover { + position:relative; + z-index:200; +} +#nav li:hover ul.sub { + left:1px; + top:22px; + background: #202020; + padding:3px; + border:1px solid #161616; + white-space:nowrap; + width:200px; + height:auto; + z-index:300; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + +} +#nav li:hover ul.sub li { + display:block; + height:22px; + position:relative; + float:left; + width:200px; + font-weight:normal; +} +#nav li:hover ul.sub li a { + display:block; + font-size:12px; + height:20px; + width:198px; + line-height:20px; + text-indent:5px; + color:#fff; + text-decoration:none; + border:1px solid #202020; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; +} +#nav li ul.sub li a.fly { + background:#202020 url(images/arrow.png) 190px 6px no-repeat; +} +#nav li:hover ul.sub li a:hover { + background:#3d3d3d; + color:#fff; + border-color:#4e4e4e; +} +#nav li:hover ul.sub li a.fly:hover { + background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; + color:#fff; +} +#nav li strong { + display:block; + font-size:12px; + height:20px; + width:198px; + line-height:20px; + margin-bottom:3px; + text-indent:6px; + color:#ff5d1a; + border-bottom:1px solid #414141; + cursor:default; + font-weight:bold; +} +#nav li:hover li:hover ul, #nav li:hover li:hover li:hover ul, #nav li:hover li:hover li:hover li:hover ul, #nav li:hover li:hover li:hover li:hover li:hover ul { + left:200px; + top:-4px; + background: #202020; + padding:3px; + border:1px solid #161616; + white-space:nowrap; + width:200px; + z-index:400; + height:auto; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} +#nav ul, #nav li:hover ul ul, #nav li:hover li:hover ul ul, #nav li:hover li:hover li:hover ul ul, #nav li:hover li:hover li:hover li:hover ul ul { + position:absolute; + left:-9999px; + top:-9999px; + width:0; + height:0; + margin:0; + padding:0; + list-style:none; +} +#nav li:hover li:hover a.fly, #nav li:hover li:hover li:hover a.fly, #nav li:hover li:hover li:hover li:hover a.fly, #nav li:hover li:hover li:hover li:hover li:hover a.fly { + background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; + color:#fff; + border-color:#4e4e4e; +} + + +#nav li:hover li:hover li:hover a.fly { + background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; + color:#fff; + border-color:#4e4e4e; +} +#nav li:hover li:hover li:hover li:hover li:hover a.fly { + background:#3d3d3d url(images/arrow_over.png) 190px 6px no-repeat; + color:#fff; + border-color:#4e4e4e; +} +#nav li:hover li:hover li a.fly, +#nav li:hover li:hover li:hover li a.fly, +#nav li:hover li:hover li:hover li:hover li a.fly { + background:#202020 url(images/arrow.png) 190px 6px no-repeat; + color:#fff; + border-color:#202020; +} + diff --git a/airtime_mvc/public/css/redmond/jquery-ui-1.8.8.custom.css b/airtime_mvc/public/css/redmond/jquery-ui-1.8.8.custom.css index 4956e096e..3ac07ab95 100644 --- a/airtime_mvc/public/css/redmond/jquery-ui-1.8.8.custom.css +++ b/airtime_mvc/public/css/redmond/jquery-ui-1.8.8.custom.css @@ -1,1524 +1,1524 @@ -/* - * jQuery UI CSS Framework 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Theming/API - */ - -/* Layout helpers -----------------------------------*/ -.ui-helper-hidden { - display: none; -} -.ui-helper-hidden-accessible { - position: absolute; - left: -99999999px; -} -.ui-helper-reset { - margin: 0; - padding: 0; - border: 0; - outline: 0; - line-height: 1.3; - text-decoration: none; - font-size: 100%; - list-style: none; -} -.ui-helper-clearfix:after { - content: "."; - display: block; - height: 0; - clear: both; - visibility: hidden; -} -.ui-helper-clearfix { - display: inline-block; -} -/* required comment for clearfix to work in Opera \*/ -* html .ui-helper-clearfix { - height:1%; -} -.ui-helper-clearfix { - display:block; -} -/* end clearfix */ -.ui-helper-zfix { - width: 100%; - height: 100%; - top: 0; - left: 0; - position: absolute; - opacity: 0; - filter:Alpha(Opacity=0); -} -/* Interaction Cues -----------------------------------*/ -.ui-state-disabled { - cursor: default !important; -} -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { - display: block; - text-indent: -99999px; - overflow: hidden; - background-repeat: no-repeat; -} -/* Misc visuals -----------------------------------*/ - -/* Overlays */ -.ui-widget-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -/* - * jQuery UI CSS Framework 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Theming/API - * - * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Helvetica,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.2em&cornerRadius=0px&bgColorHeader=ebebeb&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=50&borderColorHeader=d3d3d3&fcHeader=444444&iconColorHeader=007fb3&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=dddddd&fcContent=444444&iconColorContent=ff0084&bgColorDefault=f6f6f6&bgTextureDefault=03_highlight_soft.png&bgImgOpacityDefault=100&borderColorDefault=d3d3d3&fcDefault=3b3b3b&iconColorDefault=666666&bgColorHover=007fb3&bgTextureHover=03_highlight_soft.png&bgImgOpacityHover=25&borderColorHover=007fb3&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=ffffff&bgTextureActive=01_flat.png&bgImgOpacityActive=65&borderColorActive=d3d3d3&fcActive=007fb3&iconColorActive=454545&bgColorHighlight=eff6eb&bgTextureHighlight=01_flat.png&bgImgOpacityHighlight=55&borderColorHighlight=65a539&fcHighlight=65a539&iconColorHighlight=65a539&bgColorError=fae5e5&bgTextureError=01_flat.png&bgImgOpacityError=55&borderColorError=d00000&fcError=d00000&iconColorError=d00000&bgColorOverlay=9d9d9d&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=50&bgColorShadow=6c6c6c&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=60&thicknessShadow=6px&offsetTopShadow=-6px&offsetLeftShadow=-6px&cornerRadiusShadow=0px - */ - - -/* Component containers -----------------------------------*/ -.ui-widget { - font-family: Helvetica, Arial, sans-serif; - font-size: 1.2em; -} -.ui-widget .ui-widget { - font-size: 1em; -} -.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { - font-family: Helvetica, Arial, sans-serif; - font-size: 12px; -} -.ui-widget-content { - border: 1px solid #5b5b5b; - background: #aaaaaa url(images/ui-bg_default_aaaaaa.png) repeat-x 0 0; - color: #1c1c1c; -} -.ui-widget-content a { - color: #444444; -} -.ui-widget-header { - border: 1px solid #5b5b5b; - background: #9a9a9a url(images/ui-bg_highlight.png) 0 0 repeat-x; - color: #444444; - font-weight: bold; -} -.ui-widget-header a { - color: #444444; -} -/* Interaction states -----------------------------------*/ - - -.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { - border: 1px solid #5b5b5b; - background-color: #6e6e6e; - background: -moz-linear-gradient(top, #868686 0, #6e6e6e 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #868686), color-stop(100%, #6e6e6e)); - color: #ffffff; -} -.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { - color: #ffffff; - text-decoration: none; -} -.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { - border: 1px solid #242424; - background-color: #292929; - background: -moz-linear-gradient(top, #3b3b3b 0, #292929 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #3b3b3b), color-stop(100%, #292929)); - color: #ffffff; -} -.ui-state-hover a, .ui-state-hover a:hover { - color: #ffffff; - text-decoration: none; -} -.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { - border: 1px solid #5b5b5b; - background: #c6c6c6; - color: #000; - outline:none; -} -.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { - color: #000; - text-decoration: none; -} -.ui-widget :active { - outline: none; -} -.ui-widget-header .ui-state-default, -.ui-widget-header .ui-state-hover, -.ui-widget-header .ui-state-focus, -.ui-widget-header .ui-state-active { - font-weight:bold; -} -/* Interaction Cues -----------------------------------*/ -.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight { - border: 1px solid #65a539; - background: #eff6eb url(images/ui-bg_flat_55_eff6eb_40x100.png) 50% 50% repeat-x; - color: #65a539; -} -.ui-state-highlight a, .ui-widget-content .ui-state-highlight a, .ui-widget-header .ui-state-highlight a { - color: #65a539; -} -.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error { - border: 1px solid #d00000; - background: #fae5e5 url(images/ui-bg_flat_55_fae5e5_40x100.png) 50% 50% repeat-x; - color: #d00000; -} -.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { - color: #d00000; -} -.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { - color: #d00000; -} -.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { - font-weight: bold; -} -.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { - opacity: .7; - filter:Alpha(Opacity=70); - font-weight: normal; -} -.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { - opacity: .35; - filter:Alpha(Opacity=35); - background-image: none; -} -/* Icons -----------------------------------*/ - -/* states and images */ -.ui-icon { - width: 16px; - height: 16px; - background-image: url(images/ui-icons_ffffff_256x240.png); -} -.ui-widget-content .ui-icon { - background-image: url(images/ui-icons_ffffff_256x240.png); -} -.ui-widget-header .ui-icon { - background-image: url(images/ui-icons_ffffff_256x240.png); -} -.ui-state-default .ui-icon { - background-image: url(images/ui-icons_ffffff_256x240.png); -} -.ui-state-hover .ui-icon, .ui-state-focus .ui-icon { - background-image: url(images/ui-icons_ff5d1a_256x240.png); -} -.ui-state-active .ui-icon { - background-image: url(images/ui-icons_ff5d1a_256x240.png); -} -.ui-state-highlight .ui-icon { - background-image: url(images/ui-icons_65a539_256x240.png); -} -.ui-state-error .ui-icon, .ui-state-error-text .ui-icon { - background-image: url(images/ui-icons_d00000_256x240.png); -} -/* positioning */ -.ui-icon-carat-1-n { - background-position: 0 0; -} -.ui-icon-carat-1-ne { - background-position: -16px 0; -} -.ui-icon-carat-1-e { - background-position: -32px 0; -} -.ui-icon-carat-1-se { - background-position: -48px 0; -} -.ui-icon-carat-1-s { - background-position: -64px 0; -} -.ui-icon-carat-1-sw { - background-position: -80px 0; -} -.ui-icon-carat-1-w { - background-position: -96px 0; -} -.ui-icon-carat-1-nw { - background-position: -112px 0; -} -.ui-icon-carat-2-n-s { - background-position: -128px 0; -} -.ui-icon-carat-2-e-w { - background-position: -144px 0; -} -.ui-icon-triangle-1-n { - background-position: 0 -16px; -} -.ui-icon-triangle-1-ne { - background-position: -16px -16px; -} -.ui-icon-triangle-1-e { - background-position: -32px -16px; -} -.ui-icon-triangle-1-se { - background-position: -48px -16px; -} -.ui-icon-triangle-1-s { - background-position: -64px -16px; -} -.ui-icon-triangle-1-sw { - background-position: -80px -16px; -} -.ui-icon-triangle-1-w { - background-position: -96px -16px; -} -.ui-icon-triangle-1-nw { - background-position: -112px -16px; -} -.ui-icon-triangle-2-n-s { - background-position: -128px -16px; -} -.ui-icon-triangle-2-e-w { - background-position: -144px -16px; -} -.ui-icon-arrow-1-n { - background-position: 0 -32px; -} -.ui-icon-arrow-1-ne { - background-position: -16px -32px; -} -.ui-icon-arrow-1-e { - background-position: -32px -32px; -} -.ui-icon-arrow-1-se { - background-position: -48px -32px; -} -.ui-icon-arrow-1-s { - background-position: -64px -32px; -} -.ui-icon-arrow-1-sw { - background-position: -80px -32px; -} -.ui-icon-arrow-1-w { - background-position: -96px -32px; -} -.ui-icon-arrow-1-nw { - background-position: -112px -32px; -} -.ui-icon-arrow-2-n-s { - background-position: -128px -32px; -} -.ui-icon-arrow-2-ne-sw { - background-position: -144px -32px; -} -.ui-icon-arrow-2-e-w { - background-position: -160px -32px; -} -.ui-icon-arrow-2-se-nw { - background-position: -176px -32px; -} -.ui-icon-arrowstop-1-n { - background-position: -192px -32px; -} -.ui-icon-arrowstop-1-e { - background-position: -208px -32px; -} -.ui-icon-arrowstop-1-s { - background-position: -224px -32px; -} -.ui-icon-arrowstop-1-w { - background-position: -240px -32px; -} -.ui-icon-arrowthick-1-n { - background-position: 0 -48px; -} -.ui-icon-arrowthick-1-ne { - background-position: -16px -48px; -} -.ui-icon-arrowthick-1-e { - background-position: -32px -48px; -} -.ui-icon-arrowthick-1-se { - background-position: -48px -48px; -} -.ui-icon-arrowthick-1-s { - background-position: -64px -48px; -} -.ui-icon-arrowthick-1-sw { - background-position: -80px -48px; -} -.ui-icon-arrowthick-1-w { - background-position: -96px -48px; -} -.ui-icon-arrowthick-1-nw { - background-position: -112px -48px; -} -.ui-icon-arrowthick-2-n-s { - background-position: -128px -48px; -} -.ui-icon-arrowthick-2-ne-sw { - background-position: -144px -48px; -} -.ui-icon-arrowthick-2-e-w { - background-position: -160px -48px; -} -.ui-icon-arrowthick-2-se-nw { - background-position: -176px -48px; -} -.ui-icon-arrowthickstop-1-n { - background-position: -192px -48px; -} -.ui-icon-arrowthickstop-1-e { - background-position: -208px -48px; -} -.ui-icon-arrowthickstop-1-s { - background-position: -224px -48px; -} -.ui-icon-arrowthickstop-1-w { - background-position: -240px -48px; -} -.ui-icon-arrowreturnthick-1-w { - background-position: 0 -64px; -} -.ui-icon-arrowreturnthick-1-n { - background-position: -16px -64px; -} -.ui-icon-arrowreturnthick-1-e { - background-position: -32px -64px; -} -.ui-icon-arrowreturnthick-1-s { - background-position: -48px -64px; -} -.ui-icon-arrowreturn-1-w { - background-position: -64px -64px; -} -.ui-icon-arrowreturn-1-n { - background-position: -80px -64px; -} -.ui-icon-arrowreturn-1-e { - background-position: -96px -64px; -} -.ui-icon-arrowreturn-1-s { - background-position: -112px -64px; -} -.ui-icon-arrowrefresh-1-w { - background-position: -128px -64px; -} -.ui-icon-arrowrefresh-1-n { - background-position: -144px -64px; -} -.ui-icon-arrowrefresh-1-e { - background-position: -160px -64px; -} -.ui-icon-arrowrefresh-1-s { - background-position: -176px -64px; -} -.ui-icon-arrow-4 { - background-position: 0 -80px; -} -.ui-icon-arrow-4-diag { - background-position: -16px -80px; -} -.ui-icon-extlink { - background-position: -32px -80px; -} -.ui-icon-newwin { - background-position: -48px -80px; -} -.ui-icon-refresh { - background-position: -64px -80px; -} -.ui-icon-shuffle { - background-position: -80px -80px; -} -.ui-icon-transfer-e-w { - background-position: -96px -80px; -} -.ui-icon-transferthick-e-w { - background-position: -112px -80px; -} -.ui-icon-folder-collapsed { - background-position: 0 -96px; -} -.ui-icon-folder-open { - background-position: -16px -96px; -} -.ui-icon-document { - background-position: -32px -96px; -} -.ui-icon-document-b { - background-position: -48px -96px; -} -.ui-icon-note { - background-position: -64px -96px; -} -.ui-icon-mail-closed { - background-position: -80px -96px; -} -.ui-icon-mail-open { - background-position: -96px -96px; -} -.ui-icon-suitcase { - background-position: -112px -96px; -} -.ui-icon-comment { - background-position: -128px -96px; -} -.ui-icon-person { - background-position: -144px -96px; -} -.ui-icon-print { - background-position: -160px -96px; -} -.ui-icon-trash { - background-position: -176px -96px; -} -.ui-icon-locked { - background-position: -192px -96px; -} -.ui-icon-unlocked { - background-position: -208px -96px; -} -.ui-icon-bookmark { - background-position: -224px -96px; -} -.ui-icon-tag { - background-position: -240px -96px; -} -.ui-icon-home { - background-position: 0 -112px; -} -.ui-icon-flag { - background-position: -16px -112px; -} -.ui-icon-calendar { - background-position: -32px -112px; -} -.ui-icon-cart { - background-position: -48px -112px; -} -.ui-icon-pencil { - background-position: -64px -112px; -} -.ui-icon-clock { - background-position: -80px -112px; -} -.ui-icon-disk { - background-position: -96px -112px; -} -.ui-icon-calculator { - background-position: -112px -112px; -} -.ui-icon-zoomin { - background-position: -128px -112px; -} -.ui-icon-zoomout { - background-position: -144px -112px; -} -.ui-icon-search { - background-position: -160px -112px; -} -.ui-icon-wrench { - background-position: -176px -112px; -} -.ui-icon-gear { - background-position: -192px -112px; -} -.ui-icon-heart { - background-position: -208px -112px; -} -.ui-icon-star { - background-position: -224px -112px; -} -.ui-icon-link { - background-position: -240px -112px; -} -.ui-icon-cancel { - background-position: 0 -128px; -} -.ui-icon-plus { - background-position: -16px -128px; -} -.ui-icon-plusthick { - background-position: -32px -128px; -} -.ui-icon-minus { - background-position: -48px -128px; -} -.ui-icon-minusthick { - background-position: -64px -128px; -} -.ui-icon-close { - background-position: -80px -128px; -} -.ui-icon-closethick { - background-position: -96px -128px; -} -.ui-icon-key { - background-position: -112px -128px; -} -.ui-icon-lightbulb { - background-position: -128px -128px; -} -.ui-icon-scissors { - background-position: -144px -128px; -} -.ui-icon-clipboard { - background-position: -160px -128px; -} -.ui-icon-copy { - background-position: -176px -128px; -} -.ui-icon-contact { - background-position: -192px -128px; -} -.ui-icon-image { - background-position: -208px -128px; -} -.ui-icon-video { - background-position: -224px -128px; -} -.ui-icon-script { - background-position: -240px -128px; -} -.ui-icon-alert { - background-position: 0 -144px; -} -.ui-icon-info { - background-position: -16px -144px; -} -.ui-icon-notice { - background-position: -32px -144px; -} -.ui-icon-help { - background-position: -48px -144px; -} -.ui-icon-check { - background-position: -64px -144px; -} -.ui-icon-bullet { - background-position: -80px -144px; -} -.ui-icon-radio-off { - background-position: -96px -144px; -} -.ui-icon-radio-on { - background-position: -112px -144px; -} -.ui-icon-pin-w { - background-position: -128px -144px; -} -.ui-icon-pin-s { - background-position: -144px -144px; -} -.ui-icon-play { - background-position: 0 -160px; -} -.ui-icon-pause { - background-position: -16px -160px; -} -.ui-icon-seek-next { - background-position: -32px -160px; -} -.ui-icon-seek-prev { - background-position: -48px -160px; -} -.ui-icon-seek-end { - background-position: -64px -160px; -} -.ui-icon-seek-start { - background-position: -80px -160px; -} -/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ -.ui-icon-seek-first { - background-position: -80px -160px; -} -.ui-icon-stop { - background-position: -96px -160px; -} -.ui-icon-eject { - background-position: -112px -160px; -} -.ui-icon-volume-off { - background-position: -128px -160px; -} -.ui-icon-volume-on { - background-position: -144px -160px; -} -.ui-icon-power { - background-position: 0 -176px; -} -.ui-icon-signal-diag { - background-position: -16px -176px; -} -.ui-icon-signal { - background-position: -32px -176px; -} -.ui-icon-battery-0 { - background-position: -48px -176px; -} -.ui-icon-battery-1 { - background-position: -64px -176px; -} -.ui-icon-battery-2 { - background-position: -80px -176px; -} -.ui-icon-battery-3 { - background-position: -96px -176px; -} -.ui-icon-circle-plus { - background-position: 0 -192px; -} -.ui-icon-circle-minus { - background-position: -16px -192px; -} -.ui-icon-circle-close { - background-position: -32px -192px; -} -.ui-icon-circle-triangle-e { - background-position: -48px -192px; -} -.ui-icon-circle-triangle-s { - background-position: -64px -192px; -} -.ui-icon-circle-triangle-w { - background-position: -80px -192px; -} -.ui-icon-circle-triangle-n { - background-position: -96px -192px; -} -.ui-icon-circle-arrow-e { - background-position: -112px -192px; -} -.ui-icon-circle-arrow-s { - background-position: -128px -192px; -} -.ui-icon-circle-arrow-w { - background-position: -144px -192px; -} -.ui-icon-circle-arrow-n { - background-position: -160px -192px; -} -.ui-icon-circle-zoomin { - background-position: -176px -192px; -} -.ui-icon-circle-zoomout { - background-position: -192px -192px; -} -.ui-icon-circle-check { - background-position: -208px -192px; -} -.ui-icon-circlesmall-plus { - background-position: 0 -208px; -} -.ui-icon-circlesmall-minus { - background-position: -16px -208px; -} -.ui-icon-circlesmall-close { - background-position: -32px -208px; -} -.ui-icon-squaresmall-plus { - background-position: -48px -208px; -} -.ui-icon-squaresmall-minus { - background-position: -64px -208px; -} -.ui-icon-squaresmall-close { - background-position: -80px -208px; -} -.ui-icon-grip-dotted-vertical { - background-position: 0 -224px; -} -.ui-icon-grip-dotted-horizontal { - background-position: -16px -224px; -} -.ui-icon-grip-solid-vertical { - background-position: -32px -224px; -} -.ui-icon-grip-solid-horizontal { - background-position: -48px -224px; -} -.ui-icon-gripsmall-diagonal-se { - background-position: -64px -224px; -} -.ui-icon-grip-diagonal-se { - background-position: -80px -224px; -} -/* Misc visuals -----------------------------------*/ - -/* Corner radius */ -.ui-corner-tl { - -moz-border-radius-topleft: 0px; - -webkit-border-top-left-radius: 0px; - border-top-left-radius: 0px; -} -.ui-corner-tr { - -moz-border-radius-topright: 0px; - -webkit-border-top-right-radius: 0px; - border-top-right-radius: 0px; -} -.ui-corner-bl { - -moz-border-radius-bottomleft: 0px; - -webkit-border-bottom-left-radius: 0px; - border-bottom-left-radius: 0px; -} -.ui-corner-br { - -moz-border-radius-bottomright: 0px; - -webkit-border-bottom-right-radius: 0px; - border-bottom-right-radius: 0px; -} -.ui-corner-top { - -moz-border-radius-topleft: 0px; - -webkit-border-top-left-radius: 0px; - border-top-left-radius: 0px; - -moz-border-radius-topright: 0px; - -webkit-border-top-right-radius: 0px; - border-top-right-radius: 0px; -} -.ui-corner-bottom { - -moz-border-radius-bottomleft: 0px; - -webkit-border-bottom-left-radius: 0px; - border-bottom-left-radius: 0px; - -moz-border-radius-bottomright: 0px; - -webkit-border-bottom-right-radius: 0px; - border-bottom-right-radius: 0px; -} -.ui-corner-right { - -moz-border-radius-topright: 0px; - -webkit-border-top-right-radius: 0px; - border-top-right-radius: 0px; - -moz-border-radius-bottomright: 0px; - -webkit-border-bottom-right-radius: 0px; - border-bottom-right-radius: 0px; -} -.ui-corner-left { - -moz-border-radius-topleft: 0px; - -webkit-border-top-left-radius: 0px; - border-top-left-radius: 0px; - -moz-border-radius-bottomleft: 0px; - -webkit-border-bottom-left-radius: 0px; - border-bottom-left-radius: 0px; -} -.ui-corner-all { - -moz-border-radius: 0px; - -webkit-border-radius: 0px; - border-radius: 0px; -} -/* Overlays */ -.ui-widget-overlay { - background: #9d9d9d url(images/ui-bg_flat_0_9d9d9d_40x100.png) 50% 50% repeat-x; - opacity: .50; - filter:Alpha(Opacity=50); -} -.ui-widget-shadow { - margin: -6px 0 0 -6px; - padding: 6px; - background: #6c6c6c url(images/ui-bg_flat_0_6c6c6c_40x100.png) 50% 50% repeat-x; - opacity: .60; - filter:Alpha(Opacity=60); - -moz-border-radius: 0px; - -webkit-border-radius: 0px; - border-radius: 0px; -}/* - * jQuery UI Resizable 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Resizable#theming - */ -.ui-resizable { - position: relative; -} -.ui-resizable-handle { - position: absolute; - font-size: 0.1px; - z-index: 99999; - display: block; -} -.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { - display: none; -} -.ui-resizable-n { - cursor: n-resize; - height: 7px; - width: 100%; - top: -5px; - left: 0; -} -.ui-resizable-s { - cursor: s-resize; - height: 7px; - width: 100%; - bottom: -5px; - left: 0; -} -.ui-resizable-e { - cursor: e-resize; - width: 7px; - right: -5px; - top: 0; - height: 100%; -} -.ui-resizable-w { - cursor: w-resize; - width: 7px; - left: -5px; - top: 0; - height: 100%; -} -.ui-resizable-se { - cursor: se-resize; - width: 12px; - height: 12px; - right: 1px; - bottom: 1px; -} -.ui-resizable-sw { - cursor: sw-resize; - width: 9px; - height: 9px; - left: -5px; - bottom: -5px; -} -.ui-resizable-nw { - cursor: nw-resize; - width: 9px; - height: 9px; - left: -5px; - top: -5px; -} -.ui-resizable-ne { - cursor: ne-resize; - width: 9px; - height: 9px; - right: -5px; - top: -5px; -}/* - * jQuery UI Selectable 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Selectable#theming - */ -.ui-selectable-helper { - position: absolute; - z-index: 100; - border:1px dotted black; -} -/* - * jQuery UI Accordion 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Accordion#theming - */ -/* IE/Win - Fix animation bug - #4615 */ -.ui-accordion { - width: 100%; -} -.ui-accordion .ui-accordion-header { - cursor: pointer; - position: relative; - margin-top: 1px; - zoom: 1; - font-weight:bold; -} -.ui-accordion .ui-accordion-li-fix { - display: inline; -} -.ui-accordion .ui-accordion-header-active { - border-bottom: 0 !important; -} -.ui-accordion .ui-accordion-header a { - display: block; - font-size: 1em; - padding: .5em .5em .5em .7em; -} -.ui-accordion-icons .ui-accordion-header a { - padding-left: 2.2em; -} -.ui-accordion .ui-accordion-header .ui-icon { - position: absolute; - left: .5em; - top: 50%; - margin-top: -8px; -} -.ui-accordion .ui-accordion-content { - padding: 10px; - border-top: 0; - margin-top: -2px; - position: relative; - top: 1px; - margin-bottom: 2px; - overflow: auto; - display: none; - zoom: 1; -} -.ui-accordion .ui-accordion-content-active { - display: block; -}/* - * jQuery UI Autocomplete 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Autocomplete#theming - */ -.ui-autocomplete { - position: absolute; - cursor: default; -} -/* workarounds */ -* html .ui-autocomplete { - width:1px; -} /* without this, the menu expands to 100% in IE6 */ -/* - * jQuery UI Menu 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Menu#theming - */ -.ui-menu { - list-style:none; - padding: 2px; - margin: 0; - display:block; - float: left; -} -.ui-menu .ui-menu { - margin-top: -3px; -} -.ui-menu .ui-menu-item { - margin:0; - padding: 0; - zoom: 1; - float: left; - clear: left; - width: 100%; -} -.ui-menu .ui-menu-item a { - text-decoration:none; - display:block; - padding:.2em .4em; - line-height:1.5; - zoom:1; -} -.ui-menu .ui-menu-item a.ui-state-hover, .ui-menu .ui-menu-item a.ui-state-active { - font-weight: normal; - margin: -1px; -} -/* - * jQuery UI Button 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Button#theming - */ -.ui-button { - display: inline-block; - position: relative; - padding: 0; - margin-right: .1em; - text-decoration: none !important; - cursor: pointer; - text-align: center; - zoom: 1; - overflow: visible; -} /* the overflow property removes extra width in IE */ -.ui-button-icon-only { - width: 2.2em; -} /* to make room for the icon, a width needs to be set here */ -button.ui-button-icon-only { - width: 2.4em; -} /* button elements seem to need a little more width */ -.ui-button-icons-only { - width: 3.4em; -} -button.ui-button-icons-only { - width: 3.7em; -} -/*button text element */ -.ui-button .ui-button-text { - display: block; - line-height: 1.4; -} -.ui-button-text-only .ui-button-text { - padding: .4em 1em; -} -.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { - padding: .4em; - text-indent: -9999999px; -} -.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { - padding: .4em 1em .4em 2.1em; -} -.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { - padding: .4em 2.1em .4em 1em; -} -.ui-button-text-icons .ui-button-text { - padding-left: 2.1em; - padding-right: 2.1em; -} -/* no icon support for input elements, provide padding by default */ -input.ui-button { - padding: .4em 1em; -} -/*button icon element(s) */ -.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { - position: absolute; - top: 50%; - margin-top: -8px; - left: 0.2em; -} -.ui-button-icon-only .ui-icon { - left: 50%; - margin-left: -8px; -} -.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { - left: .5em; -} -.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { - right: .5em; -} -.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { - right: .5em; -} -/*button sets*/ -.ui-buttonset { - margin-right: 7px; -} -.ui-buttonset .ui-button { - margin-left: 0; - margin-right: -.3em; -} - -/* workarounds */ - -button.ui-button.::-moz-focus-inner { - border: 0; - padding: 0; -} - - /* reset extra padding in Firefox */ -/* - * jQuery UI Dialog 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Dialog#theming - */ -.ui-dialog { - position: absolute; - padding: .4em; - width: 300px; - overflow: hidden; - border-width: 3px; -} -.ui-dialog .ui-dialog-titlebar { - padding: 6px 8px 6px 8px; - position: relative; -} -.ui-dialog .ui-dialog-title { - float: left; - margin: .1em 16px .2em 0; -} -.ui-dialog .ui-dialog-titlebar-close { - position: absolute; - right: .3em; - top: 50%; - width: 19px; - margin: -10px 0 0 0; - padding: 1px; - height: 18px; -} -.ui-dialog .ui-dialog-titlebar-close span { - display: block; - margin: 1px; -} -.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { - padding: 0; -} -.ui-dialog .ui-dialog-content { - position: relative; - border: 0; - padding: .5em 1em; - background: none; - overflow: auto; - zoom: 1; - -} -.ui-dialog .ui-dialog-buttonpane { - text-align: left; - border-width: 1px 0 0 0; - background: none; - margin: .5em 0 0 0; - margin: 0.3em -0.4em 0; - padding: 0.3em 1em 0 0.4em; - border-color: #9f9f9f; -} -.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { - float: right; -} -.ui-dialog .ui-dialog-buttonpane button { - margin: .5em .4em .5em 0; - cursor: pointer; -} -.ui-dialog .ui-resizable-se { - width: 14px; - height: 14px; - right: 3px; - bottom: 3px; -} -.ui-draggable .ui-dialog-titlebar { - cursor: move; -} -/* - * jQuery UI Slider 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Slider#theming - */ -.ui-slider { - position: relative; - text-align: left; -} -.ui-slider .ui-slider-handle { - position: absolute; - z-index: 2; - width: 1.2em; - height: 1.2em; - cursor: default; -} -.ui-slider .ui-slider-range { - position: absolute; - z-index: 1; - font-size: .7em; - display: block; - border: 0; - background-position: 0 0; -} -.ui-slider-horizontal { - height: .8em; -} -.ui-slider-horizontal .ui-slider-handle { - top: -.3em; - margin-left: -.6em; -} -.ui-slider-horizontal .ui-slider-range { - top: 0; - height: 100%; -} -.ui-slider-horizontal .ui-slider-range-min { - left: 0; -} -.ui-slider-horizontal .ui-slider-range-max { - right: 0; -} -.ui-slider-vertical { - width: .8em; - height: 100px; -} -.ui-slider-vertical .ui-slider-handle { - left: -.3em; - margin-left: 0; - margin-bottom: -.6em; -} -.ui-slider-vertical .ui-slider-range { - left: 0; - width: 100%; -} -.ui-slider-vertical .ui-slider-range-min { - bottom: 0; -} -.ui-slider-vertical .ui-slider-range-max { - top: 0; -}/* - * jQuery UI Tabs 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Tabs#theming - */ -.ui-tabs { - position: relative; - padding: 4px; - zoom: 1; -} /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ -.ui-tabs .ui-tabs-nav { - margin: 0; - padding: .2em .2em 0; -} -.ui-tabs .ui-tabs-nav li { - list-style: none; - float: left; - position: relative; - top: 1px; - margin: 0 .2em 1px 0; - border-bottom: 0 !important; - padding: 0; - white-space: nowrap; -} -.ui-tabs .ui-tabs-nav li a { - float: left; - padding: .5em 1em; - text-decoration: none; -} -.ui-tabs .ui-tabs-nav li.ui-tabs-selected { - margin-bottom: 0; - padding-bottom: 1px; -} -.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { - cursor: text; -} -.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { - cursor: pointer; -} /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ -.ui-tabs .ui-tabs-panel { - display: block; - border-width: 0; - /*padding: 1em 1.4em;*/ - background: none; -} -.ui-tabs .ui-tabs-hide { - display: none !important; -} -/* - * jQuery UI Datepicker 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Datepicker#theming - */ -.ui-datepicker { - width: 17em; - padding: .2em .2em 0; -} -.ui-datepicker .ui-datepicker-header { - position:relative; - padding:.2em 0; -} -.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { - position:absolute; - top: 2px; - width: 1.8em; - height: 1.8em; -} -.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { - top: 1px; -} -.ui-datepicker .ui-datepicker-prev { - left:2px; -} -.ui-datepicker .ui-datepicker-next { - right:2px; -} -.ui-datepicker .ui-datepicker-prev-hover { - left:1px; -} -.ui-datepicker .ui-datepicker-next-hover { - right:1px; -} -.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { - display: block; - position: absolute; - left: 50%; - margin-left: -8px; - top: 50%; - margin-top: -8px; -} -.ui-datepicker .ui-datepicker-title { - margin: 0 2.3em; - line-height: 1.8em; - text-align: center; -} -.ui-datepicker .ui-datepicker-title select { - font-size:1em; - margin:1px 0; -} -.ui-datepicker select.ui-datepicker-month-year { - width: 100%; -} -.ui-datepicker select.ui-datepicker-month, .ui-datepicker select.ui-datepicker-year { - width: 49%; -} -.ui-datepicker table { - width: 100%; - font-size: .9em; - border-collapse: collapse; - margin:0 0 .4em; -} -.ui-datepicker th { - padding: .7em .3em; - text-align: center; - font-weight: bold; - border: 0; -} -.ui-datepicker td { - border: 0; - padding: 1px; -} -.ui-datepicker td span, .ui-datepicker td a { - display: block; - padding: .2em; - text-align: right; - text-decoration: none; -} -.ui-datepicker .ui-datepicker-buttonpane { - background-image: none; - margin: .7em 0 0 0; - padding:0 .2em; - border-left: 0; - border-right: 0; - border-bottom: 0; -} -.ui-datepicker .ui-datepicker-buttonpane button { - float: right; - margin: .5em .2em .4em; - cursor: pointer; - padding: .2em .6em .3em .6em; - width:auto; - overflow:visible; -} -.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { - float:left; -} -/* with multiple calendars */ -.ui-datepicker.ui-datepicker-multi { - width:auto; -} -.ui-datepicker-multi .ui-datepicker-group { - float:left; -} -.ui-datepicker-multi .ui-datepicker-group table { - width:95%; - margin:0 auto .4em; -} -.ui-datepicker-multi-2 .ui-datepicker-group { - width:50%; -} -.ui-datepicker-multi-3 .ui-datepicker-group { - width:33.3%; -} -.ui-datepicker-multi-4 .ui-datepicker-group { - width:25%; -} -.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { - border-left-width:0; -} -.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { - border-left-width:0; -} -.ui-datepicker-multi .ui-datepicker-buttonpane { - clear:left; -} -.ui-datepicker-row-break { - clear:both; - width:100%; -} -/* RTL support */ -.ui-datepicker-rtl { - direction: rtl; -} -.ui-datepicker-rtl .ui-datepicker-prev { - right: 2px; - left: auto; -} -.ui-datepicker-rtl .ui-datepicker-next { - left: 2px; - right: auto; -} -.ui-datepicker-rtl .ui-datepicker-prev:hover { - right: 1px; - left: auto; -} -.ui-datepicker-rtl .ui-datepicker-next:hover { - left: 1px; - right: auto; -} -.ui-datepicker-rtl .ui-datepicker-buttonpane { - clear:right; -} -.ui-datepicker-rtl .ui-datepicker-buttonpane button { - float: left; -} -.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { - float:right; -} -.ui-datepicker-rtl .ui-datepicker-group { - float:right; -} -.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { - border-right-width:0; - border-left-width:1px; -} -.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { - border-right-width:0; - border-left-width:1px; -} -/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ -.ui-datepicker-cover { - display: none; /*sorry for IE5*/ - display/**/: block; /*sorry for IE5*/ - position: absolute; /*must have*/ - z-index: -1; /*must have*/ - filter: mask(); /*must have*/ - top: -4px; /*must have*/ - left: -4px; /*must have*/ - width: 200px; /*must have*/ - height: 200px; /*must have*/ -}/* - * jQuery UI Progressbar 1.8.6 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Progressbar#theming - */ -.ui-progressbar { - height:2em; - text-align: left; -} -.ui-progressbar .ui-progressbar-value { - margin: -1px; - height:100%; -} - - -.ui-datepicker { - display:none; +/* + * jQuery UI CSS Framework 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + */ + +/* Layout helpers +----------------------------------*/ +.ui-helper-hidden { + display: none; +} +.ui-helper-hidden-accessible { + position: absolute; + left: -99999999px; +} +.ui-helper-reset { + margin: 0; + padding: 0; + border: 0; + outline: 0; + line-height: 1.3; + text-decoration: none; + font-size: 100%; + list-style: none; +} +.ui-helper-clearfix:after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +.ui-helper-clearfix { + display: inline-block; +} +/* required comment for clearfix to work in Opera \*/ +* html .ui-helper-clearfix { + height:1%; +} +.ui-helper-clearfix { + display:block; +} +/* end clearfix */ +.ui-helper-zfix { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + opacity: 0; + filter:Alpha(Opacity=0); +} +/* Interaction Cues +----------------------------------*/ +.ui-state-disabled { + cursor: default !important; +} +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + display: block; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; +} +/* Misc visuals +----------------------------------*/ + +/* Overlays */ +.ui-widget-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +/* + * jQuery UI CSS Framework 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Theming/API + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Helvetica,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.2em&cornerRadius=0px&bgColorHeader=ebebeb&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=50&borderColorHeader=d3d3d3&fcHeader=444444&iconColorHeader=007fb3&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=dddddd&fcContent=444444&iconColorContent=ff0084&bgColorDefault=f6f6f6&bgTextureDefault=03_highlight_soft.png&bgImgOpacityDefault=100&borderColorDefault=d3d3d3&fcDefault=3b3b3b&iconColorDefault=666666&bgColorHover=007fb3&bgTextureHover=03_highlight_soft.png&bgImgOpacityHover=25&borderColorHover=007fb3&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=ffffff&bgTextureActive=01_flat.png&bgImgOpacityActive=65&borderColorActive=d3d3d3&fcActive=007fb3&iconColorActive=454545&bgColorHighlight=eff6eb&bgTextureHighlight=01_flat.png&bgImgOpacityHighlight=55&borderColorHighlight=65a539&fcHighlight=65a539&iconColorHighlight=65a539&bgColorError=fae5e5&bgTextureError=01_flat.png&bgImgOpacityError=55&borderColorError=d00000&fcError=d00000&iconColorError=d00000&bgColorOverlay=9d9d9d&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=50&bgColorShadow=6c6c6c&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=60&thicknessShadow=6px&offsetTopShadow=-6px&offsetLeftShadow=-6px&cornerRadiusShadow=0px + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Helvetica, Arial, sans-serif; + font-size: 1.2em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; +} +.ui-widget-content { + border: 1px solid #5b5b5b; + background: #aaaaaa url(images/ui-bg_default_aaaaaa.png) repeat-x 0 0; + color: #1c1c1c; +} +.ui-widget-content a { + color: #444444; +} +.ui-widget-header { + border: 1px solid #5b5b5b; + background: #9a9a9a url(images/ui-bg_highlight.png) 0 0 repeat-x; + color: #444444; + font-weight: bold; +} +.ui-widget-header a { + color: #444444; +} +/* Interaction states +----------------------------------*/ + + +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { + border: 1px solid #5b5b5b; + background-color: #6e6e6e; + background: -moz-linear-gradient(top, #868686 0, #6e6e6e 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #868686), color-stop(100%, #6e6e6e)); + color: #ffffff; +} +.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { + color: #ffffff; + text-decoration: none; +} +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { + border: 1px solid #242424; + background-color: #292929; + background: -moz-linear-gradient(top, #3b3b3b 0, #292929 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #3b3b3b), color-stop(100%, #292929)); + color: #ffffff; +} +.ui-state-hover a, .ui-state-hover a:hover { + color: #ffffff; + text-decoration: none; +} +.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { + border: 1px solid #5b5b5b; + background: #c6c6c6; + color: #000; + outline:none; +} +.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { + color: #000; + text-decoration: none; +} +.ui-widget :active { + outline: none; +} +.ui-widget-header .ui-state-default, +.ui-widget-header .ui-state-hover, +.ui-widget-header .ui-state-focus, +.ui-widget-header .ui-state-active { + font-weight:bold; +} +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight { + border: 1px solid #65a539; + background: #eff6eb url(images/ui-bg_flat_55_eff6eb_40x100.png) 50% 50% repeat-x; + color: #65a539; +} +.ui-state-highlight a, .ui-widget-content .ui-state-highlight a, .ui-widget-header .ui-state-highlight a { + color: #65a539; +} +.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error { + border: 1px solid #d00000; + background: #fae5e5 url(images/ui-bg_flat_55_fae5e5_40x100.png) 50% 50% repeat-x; + color: #d00000; +} +.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { + color: #d00000; +} +.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { + color: #d00000; +} +.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); + font-weight: normal; +} +.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); + background-image: none; +} +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; + background-image: url(images/ui-icons_ffffff_256x240.png); +} +.ui-widget-content .ui-icon { + background-image: url(images/ui-icons_ffffff_256x240.png); +} +.ui-widget-header .ui-icon { + background-image: url(images/ui-icons_ffffff_256x240.png); +} +.ui-state-default .ui-icon { + background-image: url(images/ui-icons_ffffff_256x240.png); +} +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon { + background-image: url(images/ui-icons_ff5d1a_256x240.png); +} +.ui-state-active .ui-icon { + background-image: url(images/ui-icons_ff5d1a_256x240.png); +} +.ui-state-highlight .ui-icon { + background-image: url(images/ui-icons_65a539_256x240.png); +} +.ui-state-error .ui-icon, .ui-state-error-text .ui-icon { + background-image: url(images/ui-icons_d00000_256x240.png); +} +/* positioning */ +.ui-icon-carat-1-n { + background-position: 0 0; +} +.ui-icon-carat-1-ne { + background-position: -16px 0; +} +.ui-icon-carat-1-e { + background-position: -32px 0; +} +.ui-icon-carat-1-se { + background-position: -48px 0; +} +.ui-icon-carat-1-s { + background-position: -64px 0; +} +.ui-icon-carat-1-sw { + background-position: -80px 0; +} +.ui-icon-carat-1-w { + background-position: -96px 0; +} +.ui-icon-carat-1-nw { + background-position: -112px 0; +} +.ui-icon-carat-2-n-s { + background-position: -128px 0; +} +.ui-icon-carat-2-e-w { + background-position: -144px 0; +} +.ui-icon-triangle-1-n { + background-position: 0 -16px; +} +.ui-icon-triangle-1-ne { + background-position: -16px -16px; +} +.ui-icon-triangle-1-e { + background-position: -32px -16px; +} +.ui-icon-triangle-1-se { + background-position: -48px -16px; +} +.ui-icon-triangle-1-s { + background-position: -64px -16px; +} +.ui-icon-triangle-1-sw { + background-position: -80px -16px; +} +.ui-icon-triangle-1-w { + background-position: -96px -16px; +} +.ui-icon-triangle-1-nw { + background-position: -112px -16px; +} +.ui-icon-triangle-2-n-s { + background-position: -128px -16px; +} +.ui-icon-triangle-2-e-w { + background-position: -144px -16px; +} +.ui-icon-arrow-1-n { + background-position: 0 -32px; +} +.ui-icon-arrow-1-ne { + background-position: -16px -32px; +} +.ui-icon-arrow-1-e { + background-position: -32px -32px; +} +.ui-icon-arrow-1-se { + background-position: -48px -32px; +} +.ui-icon-arrow-1-s { + background-position: -64px -32px; +} +.ui-icon-arrow-1-sw { + background-position: -80px -32px; +} +.ui-icon-arrow-1-w { + background-position: -96px -32px; +} +.ui-icon-arrow-1-nw { + background-position: -112px -32px; +} +.ui-icon-arrow-2-n-s { + background-position: -128px -32px; +} +.ui-icon-arrow-2-ne-sw { + background-position: -144px -32px; +} +.ui-icon-arrow-2-e-w { + background-position: -160px -32px; +} +.ui-icon-arrow-2-se-nw { + background-position: -176px -32px; +} +.ui-icon-arrowstop-1-n { + background-position: -192px -32px; +} +.ui-icon-arrowstop-1-e { + background-position: -208px -32px; +} +.ui-icon-arrowstop-1-s { + background-position: -224px -32px; +} +.ui-icon-arrowstop-1-w { + background-position: -240px -32px; +} +.ui-icon-arrowthick-1-n { + background-position: 0 -48px; +} +.ui-icon-arrowthick-1-ne { + background-position: -16px -48px; +} +.ui-icon-arrowthick-1-e { + background-position: -32px -48px; +} +.ui-icon-arrowthick-1-se { + background-position: -48px -48px; +} +.ui-icon-arrowthick-1-s { + background-position: -64px -48px; +} +.ui-icon-arrowthick-1-sw { + background-position: -80px -48px; +} +.ui-icon-arrowthick-1-w { + background-position: -96px -48px; +} +.ui-icon-arrowthick-1-nw { + background-position: -112px -48px; +} +.ui-icon-arrowthick-2-n-s { + background-position: -128px -48px; +} +.ui-icon-arrowthick-2-ne-sw { + background-position: -144px -48px; +} +.ui-icon-arrowthick-2-e-w { + background-position: -160px -48px; +} +.ui-icon-arrowthick-2-se-nw { + background-position: -176px -48px; +} +.ui-icon-arrowthickstop-1-n { + background-position: -192px -48px; +} +.ui-icon-arrowthickstop-1-e { + background-position: -208px -48px; +} +.ui-icon-arrowthickstop-1-s { + background-position: -224px -48px; +} +.ui-icon-arrowthickstop-1-w { + background-position: -240px -48px; +} +.ui-icon-arrowreturnthick-1-w { + background-position: 0 -64px; +} +.ui-icon-arrowreturnthick-1-n { + background-position: -16px -64px; +} +.ui-icon-arrowreturnthick-1-e { + background-position: -32px -64px; +} +.ui-icon-arrowreturnthick-1-s { + background-position: -48px -64px; +} +.ui-icon-arrowreturn-1-w { + background-position: -64px -64px; +} +.ui-icon-arrowreturn-1-n { + background-position: -80px -64px; +} +.ui-icon-arrowreturn-1-e { + background-position: -96px -64px; +} +.ui-icon-arrowreturn-1-s { + background-position: -112px -64px; +} +.ui-icon-arrowrefresh-1-w { + background-position: -128px -64px; +} +.ui-icon-arrowrefresh-1-n { + background-position: -144px -64px; +} +.ui-icon-arrowrefresh-1-e { + background-position: -160px -64px; +} +.ui-icon-arrowrefresh-1-s { + background-position: -176px -64px; +} +.ui-icon-arrow-4 { + background-position: 0 -80px; +} +.ui-icon-arrow-4-diag { + background-position: -16px -80px; +} +.ui-icon-extlink { + background-position: -32px -80px; +} +.ui-icon-newwin { + background-position: -48px -80px; +} +.ui-icon-refresh { + background-position: -64px -80px; +} +.ui-icon-shuffle { + background-position: -80px -80px; +} +.ui-icon-transfer-e-w { + background-position: -96px -80px; +} +.ui-icon-transferthick-e-w { + background-position: -112px -80px; +} +.ui-icon-folder-collapsed { + background-position: 0 -96px; +} +.ui-icon-folder-open { + background-position: -16px -96px; +} +.ui-icon-document { + background-position: -32px -96px; +} +.ui-icon-document-b { + background-position: -48px -96px; +} +.ui-icon-note { + background-position: -64px -96px; +} +.ui-icon-mail-closed { + background-position: -80px -96px; +} +.ui-icon-mail-open { + background-position: -96px -96px; +} +.ui-icon-suitcase { + background-position: -112px -96px; +} +.ui-icon-comment { + background-position: -128px -96px; +} +.ui-icon-person { + background-position: -144px -96px; +} +.ui-icon-print { + background-position: -160px -96px; +} +.ui-icon-trash { + background-position: -176px -96px; +} +.ui-icon-locked { + background-position: -192px -96px; +} +.ui-icon-unlocked { + background-position: -208px -96px; +} +.ui-icon-bookmark { + background-position: -224px -96px; +} +.ui-icon-tag { + background-position: -240px -96px; +} +.ui-icon-home { + background-position: 0 -112px; +} +.ui-icon-flag { + background-position: -16px -112px; +} +.ui-icon-calendar { + background-position: -32px -112px; +} +.ui-icon-cart { + background-position: -48px -112px; +} +.ui-icon-pencil { + background-position: -64px -112px; +} +.ui-icon-clock { + background-position: -80px -112px; +} +.ui-icon-disk { + background-position: -96px -112px; +} +.ui-icon-calculator { + background-position: -112px -112px; +} +.ui-icon-zoomin { + background-position: -128px -112px; +} +.ui-icon-zoomout { + background-position: -144px -112px; +} +.ui-icon-search { + background-position: -160px -112px; +} +.ui-icon-wrench { + background-position: -176px -112px; +} +.ui-icon-gear { + background-position: -192px -112px; +} +.ui-icon-heart { + background-position: -208px -112px; +} +.ui-icon-star { + background-position: -224px -112px; +} +.ui-icon-link { + background-position: -240px -112px; +} +.ui-icon-cancel { + background-position: 0 -128px; +} +.ui-icon-plus { + background-position: -16px -128px; +} +.ui-icon-plusthick { + background-position: -32px -128px; +} +.ui-icon-minus { + background-position: -48px -128px; +} +.ui-icon-minusthick { + background-position: -64px -128px; +} +.ui-icon-close { + background-position: -80px -128px; +} +.ui-icon-closethick { + background-position: -96px -128px; +} +.ui-icon-key { + background-position: -112px -128px; +} +.ui-icon-lightbulb { + background-position: -128px -128px; +} +.ui-icon-scissors { + background-position: -144px -128px; +} +.ui-icon-clipboard { + background-position: -160px -128px; +} +.ui-icon-copy { + background-position: -176px -128px; +} +.ui-icon-contact { + background-position: -192px -128px; +} +.ui-icon-image { + background-position: -208px -128px; +} +.ui-icon-video { + background-position: -224px -128px; +} +.ui-icon-script { + background-position: -240px -128px; +} +.ui-icon-alert { + background-position: 0 -144px; +} +.ui-icon-info { + background-position: -16px -144px; +} +.ui-icon-notice { + background-position: -32px -144px; +} +.ui-icon-help { + background-position: -48px -144px; +} +.ui-icon-check { + background-position: -64px -144px; +} +.ui-icon-bullet { + background-position: -80px -144px; +} +.ui-icon-radio-off { + background-position: -96px -144px; +} +.ui-icon-radio-on { + background-position: -112px -144px; +} +.ui-icon-pin-w { + background-position: -128px -144px; +} +.ui-icon-pin-s { + background-position: -144px -144px; +} +.ui-icon-play { + background-position: 0 -160px; +} +.ui-icon-pause { + background-position: -16px -160px; +} +.ui-icon-seek-next { + background-position: -32px -160px; +} +.ui-icon-seek-prev { + background-position: -48px -160px; +} +.ui-icon-seek-end { + background-position: -64px -160px; +} +.ui-icon-seek-start { + background-position: -80px -160px; +} +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { + background-position: -80px -160px; +} +.ui-icon-stop { + background-position: -96px -160px; +} +.ui-icon-eject { + background-position: -112px -160px; +} +.ui-icon-volume-off { + background-position: -128px -160px; +} +.ui-icon-volume-on { + background-position: -144px -160px; +} +.ui-icon-power { + background-position: 0 -176px; +} +.ui-icon-signal-diag { + background-position: -16px -176px; +} +.ui-icon-signal { + background-position: -32px -176px; +} +.ui-icon-battery-0 { + background-position: -48px -176px; +} +.ui-icon-battery-1 { + background-position: -64px -176px; +} +.ui-icon-battery-2 { + background-position: -80px -176px; +} +.ui-icon-battery-3 { + background-position: -96px -176px; +} +.ui-icon-circle-plus { + background-position: 0 -192px; +} +.ui-icon-circle-minus { + background-position: -16px -192px; +} +.ui-icon-circle-close { + background-position: -32px -192px; +} +.ui-icon-circle-triangle-e { + background-position: -48px -192px; +} +.ui-icon-circle-triangle-s { + background-position: -64px -192px; +} +.ui-icon-circle-triangle-w { + background-position: -80px -192px; +} +.ui-icon-circle-triangle-n { + background-position: -96px -192px; +} +.ui-icon-circle-arrow-e { + background-position: -112px -192px; +} +.ui-icon-circle-arrow-s { + background-position: -128px -192px; +} +.ui-icon-circle-arrow-w { + background-position: -144px -192px; +} +.ui-icon-circle-arrow-n { + background-position: -160px -192px; +} +.ui-icon-circle-zoomin { + background-position: -176px -192px; +} +.ui-icon-circle-zoomout { + background-position: -192px -192px; +} +.ui-icon-circle-check { + background-position: -208px -192px; +} +.ui-icon-circlesmall-plus { + background-position: 0 -208px; +} +.ui-icon-circlesmall-minus { + background-position: -16px -208px; +} +.ui-icon-circlesmall-close { + background-position: -32px -208px; +} +.ui-icon-squaresmall-plus { + background-position: -48px -208px; +} +.ui-icon-squaresmall-minus { + background-position: -64px -208px; +} +.ui-icon-squaresmall-close { + background-position: -80px -208px; +} +.ui-icon-grip-dotted-vertical { + background-position: 0 -224px; +} +.ui-icon-grip-dotted-horizontal { + background-position: -16px -224px; +} +.ui-icon-grip-solid-vertical { + background-position: -32px -224px; +} +.ui-icon-grip-solid-horizontal { + background-position: -48px -224px; +} +.ui-icon-gripsmall-diagonal-se { + background-position: -64px -224px; +} +.ui-icon-grip-diagonal-se { + background-position: -80px -224px; +} +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-tl { + -moz-border-radius-topleft: 0px; + -webkit-border-top-left-radius: 0px; + border-top-left-radius: 0px; +} +.ui-corner-tr { + -moz-border-radius-topright: 0px; + -webkit-border-top-right-radius: 0px; + border-top-right-radius: 0px; +} +.ui-corner-bl { + -moz-border-radius-bottomleft: 0px; + -webkit-border-bottom-left-radius: 0px; + border-bottom-left-radius: 0px; +} +.ui-corner-br { + -moz-border-radius-bottomright: 0px; + -webkit-border-bottom-right-radius: 0px; + border-bottom-right-radius: 0px; +} +.ui-corner-top { + -moz-border-radius-topleft: 0px; + -webkit-border-top-left-radius: 0px; + border-top-left-radius: 0px; + -moz-border-radius-topright: 0px; + -webkit-border-top-right-radius: 0px; + border-top-right-radius: 0px; +} +.ui-corner-bottom { + -moz-border-radius-bottomleft: 0px; + -webkit-border-bottom-left-radius: 0px; + border-bottom-left-radius: 0px; + -moz-border-radius-bottomright: 0px; + -webkit-border-bottom-right-radius: 0px; + border-bottom-right-radius: 0px; +} +.ui-corner-right { + -moz-border-radius-topright: 0px; + -webkit-border-top-right-radius: 0px; + border-top-right-radius: 0px; + -moz-border-radius-bottomright: 0px; + -webkit-border-bottom-right-radius: 0px; + border-bottom-right-radius: 0px; +} +.ui-corner-left { + -moz-border-radius-topleft: 0px; + -webkit-border-top-left-radius: 0px; + border-top-left-radius: 0px; + -moz-border-radius-bottomleft: 0px; + -webkit-border-bottom-left-radius: 0px; + border-bottom-left-radius: 0px; +} +.ui-corner-all { + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + border-radius: 0px; +} +/* Overlays */ +.ui-widget-overlay { + background: #9d9d9d url(images/ui-bg_flat_0_9d9d9d_40x100.png) 50% 50% repeat-x; + opacity: .50; + filter:Alpha(Opacity=50); +} +.ui-widget-shadow { + margin: -6px 0 0 -6px; + padding: 6px; + background: #6c6c6c url(images/ui-bg_flat_0_6c6c6c_40x100.png) 50% 50% repeat-x; + opacity: .60; + filter:Alpha(Opacity=60); + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + border-radius: 0px; +}/* + * jQuery UI Resizable 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Resizable#theming + */ +.ui-resizable { + position: relative; +} +.ui-resizable-handle { + position: absolute; + font-size: 0.1px; + z-index: 99999; + display: block; +} +.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { + display: none; +} +.ui-resizable-n { + cursor: n-resize; + height: 7px; + width: 100%; + top: -5px; + left: 0; +} +.ui-resizable-s { + cursor: s-resize; + height: 7px; + width: 100%; + bottom: -5px; + left: 0; +} +.ui-resizable-e { + cursor: e-resize; + width: 7px; + right: -5px; + top: 0; + height: 100%; +} +.ui-resizable-w { + cursor: w-resize; + width: 7px; + left: -5px; + top: 0; + height: 100%; +} +.ui-resizable-se { + cursor: se-resize; + width: 12px; + height: 12px; + right: 1px; + bottom: 1px; +} +.ui-resizable-sw { + cursor: sw-resize; + width: 9px; + height: 9px; + left: -5px; + bottom: -5px; +} +.ui-resizable-nw { + cursor: nw-resize; + width: 9px; + height: 9px; + left: -5px; + top: -5px; +} +.ui-resizable-ne { + cursor: ne-resize; + width: 9px; + height: 9px; + right: -5px; + top: -5px; +}/* + * jQuery UI Selectable 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Selectable#theming + */ +.ui-selectable-helper { + position: absolute; + z-index: 100; + border:1px dotted black; +} +/* + * jQuery UI Accordion 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Accordion#theming + */ +/* IE/Win - Fix animation bug - #4615 */ +.ui-accordion { + width: 100%; +} +.ui-accordion .ui-accordion-header { + cursor: pointer; + position: relative; + margin-top: 1px; + zoom: 1; + font-weight:bold; +} +.ui-accordion .ui-accordion-li-fix { + display: inline; +} +.ui-accordion .ui-accordion-header-active { + border-bottom: 0 !important; +} +.ui-accordion .ui-accordion-header a { + display: block; + font-size: 1em; + padding: .5em .5em .5em .7em; +} +.ui-accordion-icons .ui-accordion-header a { + padding-left: 2.2em; +} +.ui-accordion .ui-accordion-header .ui-icon { + position: absolute; + left: .5em; + top: 50%; + margin-top: -8px; +} +.ui-accordion .ui-accordion-content { + padding: 10px; + border-top: 0; + margin-top: -2px; + position: relative; + top: 1px; + margin-bottom: 2px; + overflow: auto; + display: none; + zoom: 1; +} +.ui-accordion .ui-accordion-content-active { + display: block; +}/* + * jQuery UI Autocomplete 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Autocomplete#theming + */ +.ui-autocomplete { + position: absolute; + cursor: default; +} +/* workarounds */ +* html .ui-autocomplete { + width:1px; +} /* without this, the menu expands to 100% in IE6 */ +/* + * jQuery UI Menu 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Menu#theming + */ +.ui-menu { + list-style:none; + padding: 2px; + margin: 0; + display:block; + float: left; +} +.ui-menu .ui-menu { + margin-top: -3px; +} +.ui-menu .ui-menu-item { + margin:0; + padding: 0; + zoom: 1; + float: left; + clear: left; + width: 100%; +} +.ui-menu .ui-menu-item a { + text-decoration:none; + display:block; + padding:.2em .4em; + line-height:1.5; + zoom:1; +} +.ui-menu .ui-menu-item a.ui-state-hover, .ui-menu .ui-menu-item a.ui-state-active { + font-weight: normal; + margin: -1px; +} +/* + * jQuery UI Button 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Button#theming + */ +.ui-button { + display: inline-block; + position: relative; + padding: 0; + margin-right: .1em; + text-decoration: none !important; + cursor: pointer; + text-align: center; + zoom: 1; + overflow: visible; +} /* the overflow property removes extra width in IE */ +.ui-button-icon-only { + width: 2.2em; +} /* to make room for the icon, a width needs to be set here */ +button.ui-button-icon-only { + width: 2.4em; +} /* button elements seem to need a little more width */ +.ui-button-icons-only { + width: 3.4em; +} +button.ui-button-icons-only { + width: 3.7em; +} +/*button text element */ +.ui-button .ui-button-text { + display: block; + line-height: 1.4; +} +.ui-button-text-only .ui-button-text { + padding: .4em 1em; +} +.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { + padding: .4em; + text-indent: -9999999px; +} +.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { + padding: .4em 1em .4em 2.1em; +} +.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { + padding: .4em 2.1em .4em 1em; +} +.ui-button-text-icons .ui-button-text { + padding-left: 2.1em; + padding-right: 2.1em; +} +/* no icon support for input elements, provide padding by default */ +input.ui-button { + padding: .4em 1em; +} +/*button icon element(s) */ +.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { + position: absolute; + top: 50%; + margin-top: -8px; + left: 0.2em; +} +.ui-button-icon-only .ui-icon { + left: 50%; + margin-left: -8px; +} +.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { + left: .5em; +} +.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { + right: .5em; +} +.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { + right: .5em; +} +/*button sets*/ +.ui-buttonset { + margin-right: 7px; +} +.ui-buttonset .ui-button { + margin-left: 0; + margin-right: -.3em; +} + +/* workarounds */ + +button.ui-button.::-moz-focus-inner { + border: 0; + padding: 0; +} + + /* reset extra padding in Firefox */ +/* + * jQuery UI Dialog 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Dialog#theming + */ +.ui-dialog { + position: absolute; + padding: .4em; + width: 300px; + overflow: hidden; + border-width: 3px; +} +.ui-dialog .ui-dialog-titlebar { + padding: 6px 8px 6px 8px; + position: relative; +} +.ui-dialog .ui-dialog-title { + float: left; + margin: .1em 16px .2em 0; +} +.ui-dialog .ui-dialog-titlebar-close { + position: absolute; + right: .3em; + top: 50%; + width: 19px; + margin: -10px 0 0 0; + padding: 1px; + height: 18px; +} +.ui-dialog .ui-dialog-titlebar-close span { + display: block; + margin: 1px; +} +.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { + padding: 0; +} +.ui-dialog .ui-dialog-content { + position: relative; + border: 0; + padding: .5em 1em; + background: none; + overflow: auto; + zoom: 1; + +} +.ui-dialog .ui-dialog-buttonpane { + text-align: left; + border-width: 1px 0 0 0; + background: none; + margin: .5em 0 0 0; + margin: 0.3em -0.4em 0; + padding: 0.3em 1em 0 0.4em; + border-color: #9f9f9f; +} +.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { + float: right; +} +.ui-dialog .ui-dialog-buttonpane button { + margin: .5em .4em .5em 0; + cursor: pointer; +} +.ui-dialog .ui-resizable-se { + width: 14px; + height: 14px; + right: 3px; + bottom: 3px; +} +.ui-draggable .ui-dialog-titlebar { + cursor: move; +} +/* + * jQuery UI Slider 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Slider#theming + */ +.ui-slider { + position: relative; + text-align: left; +} +.ui-slider .ui-slider-handle { + position: absolute; + z-index: 2; + width: 1.2em; + height: 1.2em; + cursor: default; +} +.ui-slider .ui-slider-range { + position: absolute; + z-index: 1; + font-size: .7em; + display: block; + border: 0; + background-position: 0 0; +} +.ui-slider-horizontal { + height: .8em; +} +.ui-slider-horizontal .ui-slider-handle { + top: -.3em; + margin-left: -.6em; +} +.ui-slider-horizontal .ui-slider-range { + top: 0; + height: 100%; +} +.ui-slider-horizontal .ui-slider-range-min { + left: 0; +} +.ui-slider-horizontal .ui-slider-range-max { + right: 0; +} +.ui-slider-vertical { + width: .8em; + height: 100px; +} +.ui-slider-vertical .ui-slider-handle { + left: -.3em; + margin-left: 0; + margin-bottom: -.6em; +} +.ui-slider-vertical .ui-slider-range { + left: 0; + width: 100%; +} +.ui-slider-vertical .ui-slider-range-min { + bottom: 0; +} +.ui-slider-vertical .ui-slider-range-max { + top: 0; +}/* + * jQuery UI Tabs 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Tabs#theming + */ +.ui-tabs { + position: relative; + padding: 4px; + zoom: 1; +} /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ +.ui-tabs .ui-tabs-nav { + margin: 0; + padding: .2em .2em 0; +} +.ui-tabs .ui-tabs-nav li { + list-style: none; + float: left; + position: relative; + top: 1px; + margin: 0 .2em 1px 0; + border-bottom: 0 !important; + padding: 0; + white-space: nowrap; +} +.ui-tabs .ui-tabs-nav li a { + float: left; + padding: .5em 1em; + text-decoration: none; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-selected { + margin-bottom: 0; + padding-bottom: 1px; +} +.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { + cursor: text; +} +.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { + cursor: pointer; +} /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ +.ui-tabs .ui-tabs-panel { + display: block; + border-width: 0; + /*padding: 1em 1.4em;*/ + background: none; +} +.ui-tabs .ui-tabs-hide { + display: none !important; +} +/* + * jQuery UI Datepicker 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Datepicker#theming + */ +.ui-datepicker { + width: 17em; + padding: .2em .2em 0; +} +.ui-datepicker .ui-datepicker-header { + position:relative; + padding:.2em 0; +} +.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { + position:absolute; + top: 2px; + width: 1.8em; + height: 1.8em; +} +.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { + top: 1px; +} +.ui-datepicker .ui-datepicker-prev { + left:2px; +} +.ui-datepicker .ui-datepicker-next { + right:2px; +} +.ui-datepicker .ui-datepicker-prev-hover { + left:1px; +} +.ui-datepicker .ui-datepicker-next-hover { + right:1px; +} +.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { + display: block; + position: absolute; + left: 50%; + margin-left: -8px; + top: 50%; + margin-top: -8px; +} +.ui-datepicker .ui-datepicker-title { + margin: 0 2.3em; + line-height: 1.8em; + text-align: center; +} +.ui-datepicker .ui-datepicker-title select { + font-size:1em; + margin:1px 0; +} +.ui-datepicker select.ui-datepicker-month-year { + width: 100%; +} +.ui-datepicker select.ui-datepicker-month, .ui-datepicker select.ui-datepicker-year { + width: 49%; +} +.ui-datepicker table { + width: 100%; + font-size: .9em; + border-collapse: collapse; + margin:0 0 .4em; +} +.ui-datepicker th { + padding: .7em .3em; + text-align: center; + font-weight: bold; + border: 0; +} +.ui-datepicker td { + border: 0; + padding: 1px; +} +.ui-datepicker td span, .ui-datepicker td a { + display: block; + padding: .2em; + text-align: right; + text-decoration: none; +} +.ui-datepicker .ui-datepicker-buttonpane { + background-image: none; + margin: .7em 0 0 0; + padding:0 .2em; + border-left: 0; + border-right: 0; + border-bottom: 0; +} +.ui-datepicker .ui-datepicker-buttonpane button { + float: right; + margin: .5em .2em .4em; + cursor: pointer; + padding: .2em .6em .3em .6em; + width:auto; + overflow:visible; +} +.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { + float:left; +} +/* with multiple calendars */ +.ui-datepicker.ui-datepicker-multi { + width:auto; +} +.ui-datepicker-multi .ui-datepicker-group { + float:left; +} +.ui-datepicker-multi .ui-datepicker-group table { + width:95%; + margin:0 auto .4em; +} +.ui-datepicker-multi-2 .ui-datepicker-group { + width:50%; +} +.ui-datepicker-multi-3 .ui-datepicker-group { + width:33.3%; +} +.ui-datepicker-multi-4 .ui-datepicker-group { + width:25%; +} +.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { + border-left-width:0; +} +.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { + border-left-width:0; +} +.ui-datepicker-multi .ui-datepicker-buttonpane { + clear:left; +} +.ui-datepicker-row-break { + clear:both; + width:100%; +} +/* RTL support */ +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-rtl .ui-datepicker-prev { + right: 2px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next { + left: 2px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-prev:hover { + right: 1px; + left: auto; +} +.ui-datepicker-rtl .ui-datepicker-next:hover { + left: 1px; + right: auto; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane { + clear:right; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button { + float: left; +} +.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { + float:right; +} +.ui-datepicker-rtl .ui-datepicker-group { + float:right; +} +.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { + border-right-width:0; + border-left-width:1px; +} +.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { + border-right-width:0; + border-left-width:1px; +} +/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ +.ui-datepicker-cover { + display: none; /*sorry for IE5*/ + display/**/: block; /*sorry for IE5*/ + position: absolute; /*must have*/ + z-index: -1; /*must have*/ + filter: mask(); /*must have*/ + top: -4px; /*must have*/ + left: -4px; /*must have*/ + width: 200px; /*must have*/ + height: 200px; /*must have*/ +}/* + * jQuery UI Progressbar 1.8.6 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Progressbar#theming + */ +.ui-progressbar { + height:2em; + text-align: left; +} +.ui-progressbar .ui-progressbar-value { + margin: -1px; + height:100%; +} + + +.ui-datepicker { + display:none; } \ No newline at end of file diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css index b14b77251..1b0a12dd3 100644 --- a/airtime_mvc/public/css/styles.css +++ b/airtime_mvc/public/css/styles.css @@ -105,7 +105,7 @@ select { } .airtime_auth_help_icon, .custom_auth_help_icon, .stream_username_help_icon, -.playlist_type_help_icon, .master_username_help_icon { +.playlist_type_help_icon, .master_username_help_icon, .repeat_tracks_help_icon{ cursor: help; position: relative; display:inline-block; zoom:1; @@ -514,6 +514,9 @@ table.library-get-file-md.table-small{ /***** SMART BLOCK SPECIFIC STYLES BEGIN *****/ +.db-logic-label{ + font-size:11px; +} .sp-invisible{ visibility: hidden; } diff --git a/airtime_mvc/public/js/airtime/common/audioplaytest.js b/airtime_mvc/public/js/airtime/common/audioplaytest.js index 76bc5d7c1..f96ad09af 100644 --- a/airtime_mvc/public/js/airtime/common/audioplaytest.js +++ b/airtime_mvc/public/js/airtime/common/audioplaytest.js @@ -14,5 +14,6 @@ function isAudioSupported(mime){ //Note that checking the navigator.mimeTypes value does not work for IE7, but the alternative //is adding a javascript library to do the work for you, which seems like overkill.... return (!!audio.canPlayType && audio.canPlayType(bMime) != "") || - (mime.indexOf("mp3") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined); + (mime.indexOf("mp3") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined) || + (mime.indexOf("mp4") != -1 && navigator.mimeTypes ["application/x-shockwave-flash"] != undefined); } diff --git a/airtime_mvc/public/js/airtime/common/common.js b/airtime_mvc/public/js/airtime/common/common.js index 3125d8d55..0d9883b66 100644 --- a/airtime_mvc/public/js/airtime/common/common.js +++ b/airtime_mvc/public/js/airtime/common/common.js @@ -54,7 +54,10 @@ function open_audio_preview(type, id, audioFileTitle, audioFileArtist) { audioFileTitle = audioFileTitle.substring(0,index); } - openPreviewWindow('audiopreview/audio-preview/audioFileID/'+id+'/audioFileArtist/'+encodeURIComponent(audioFileArtist)+'/audioFileTitle/'+encodeURIComponent(audioFileTitle)+'/type/'+type); + // The reason that we need to encode artist and title string is that + // sometime they contain '/' or '\' and apache reject %2f or %5f + // so the work around is to encode it twice. + openPreviewWindow('audiopreview/audio-preview/audioFileID/'+id+'/audioFileArtist/'+encodeURIComponent(encodeURIComponent(audioFileArtist))+'/audioFileTitle/'+encodeURIComponent(encodeURIComponent(audioFileTitle))+'/type/'+type); _preview_window.focus(); } 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 4b139f66e..fd9851f33 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_playlistbuilder.js @@ -37,6 +37,11 @@ var AIRTIME = (function(AIRTIME) { var $nRow = $(nRow); if (aData.ftype === "audioclip") { $nRow.addClass("lib-audio"); + $image = $nRow.find('td.library_type'); + if (!isAudioSupported(aData.mime)) { + $image.html(''); + aData.image = ''; + } } else if (aData.ftype === "stream") { $nRow.addClass("lib-stream"); } else if (aData.ftype === "block") { @@ -64,8 +69,9 @@ var AIRTIME = (function(AIRTIME) { helper : function() { var $el = $(this), selected = mod - .getChosenAudioFilesLength(), container, message, li = $("#side_playlist ul[id='spl_sortable'] li:first"), width = li - .width(), height = 55; + .getChosenAudioFilesLength(), container, message, li = $("#side_playlist ul[id='spl_sortable'] li:first"), + width = li.width(), height = 55; + if (width > 798) width = 798; // dragging an element that has an unselected // checkbox. 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 67151e869..ba03586d0 100644 --- a/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js +++ b/airtime_mvc/public/js/airtime/library/events/library_showbuilder.js @@ -29,6 +29,11 @@ var AIRTIME = (function(AIRTIME) { if (aData.ftype === "audioclip") { $nRow.addClass("lib-audio"); + $image = $nRow.find('td.library_type'); + if (!isAudioSupported(aData.mime)) { + $image.html(''); + aData.image = ''; + } } else if (aData.ftype === "stream") { $nRow.addClass("lib-stream"); } else { diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index a832134c1..b463610bc 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -70,7 +70,7 @@ var AIRTIME = (function(AIRTIME) { }; mod.getChosenAudioFilesLength = function(){ - //var files = Object.keys(chosenItems), + // var files = Object.keys(chosenItems), var files, $trs, cItem, @@ -215,7 +215,7 @@ var AIRTIME = (function(AIRTIME) { mod.removeFromChosen = function($el) { var id = $el.attr("id"); - //used to not keep dragged items selected. + // used to not keep dragged items selected. if (!$el.hasClass(LIB_SELECTED_CLASS)) { delete chosenItems[id]; } @@ -252,11 +252,11 @@ var AIRTIME = (function(AIRTIME) { }; /* - * selects all items which the user can currently see. - * (behaviour taken from gmail) + * selects all items which the user can currently see. (behaviour taken from + * gmail) * - * by default the items are selected in reverse order - * so we need to reverse it back + * by default the items are selected in reverse order so we need to reverse + * it back */ mod.selectCurrentPage = function() { $.fn.reverse = [].reverse; @@ -276,8 +276,8 @@ var AIRTIME = (function(AIRTIME) { }; /* - * deselects all items that the user can currently see. - * (behaviour taken from gmail) + * deselects all items that the user can currently see. (behaviour taken + * from gmail) */ mod.deselectCurrentPage = function() { var $inputs = $libTable.find("tbody input:checkbox"), @@ -328,7 +328,7 @@ var AIRTIME = (function(AIRTIME) { temp, aMedia = []; - //process selected files/playlists. + // process selected files/playlists. for (item in aData) { temp = aData[item]; if (temp !== null && temp.hasOwnProperty('id') ) { @@ -433,36 +433,37 @@ var AIRTIME = (function(AIRTIME) { oTable = $libTable.dataTable( { - //put hidden columns at the top to insure they can never be visible on the table through column reordering. + // put hidden columns at the top to insure they can never be visible + // on the table through column reordering. "aoColumns": [ - /* ftype */ { "sTitle" : "" , "mDataProp" : "ftype" , "bSearchable" : false , "bVisible" : false } , - /* 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" : 0 } , - /* Title */ { "sTitle" : "Title" , "mDataProp" : "track_title" , "sClass" : "library_title" , "sWidth" : "170px" } , - /* Creator */ { "sTitle" : "Creator" , "mDataProp" : "artist_name" , "sClass" : "library_creator" , "sWidth" : "160px" } , - /* Album */ { "sTitle" : "Album" , "mDataProp" : "album_title" , "sClass" : "library_album" , "sWidth" : "150px" } , - /* Bit Rate */ { "sTitle" : "Bit Rate" , "mDataProp" : "bit_rate" , "bVisible" : false , "sClass" : "library_bitrate" , "sWidth" : "80px" }, - /* BPM */ { "sTitle" : "BPM" , "mDataProp" : "bpm" , "bVisible" : false , "sClass" : "library_bpm" , "sWidth" : "50px" }, - /* Composer */ { "sTitle" : "Composer" , "mDataProp" : "composer" , "bVisible" : false , "sClass" : "library_composer" , "sWidth" : "150px" }, - /* Conductor */ { "sTitle" : "Conductor" , "mDataProp" : "conductor" , "bVisible" : false , "sClass" : "library_conductor" , "sWidth" : "125px" }, - /* Copyright */ { "sTitle" : "Copyright" , "mDataProp" : "copyright" , "bVisible" : false , "sClass" : "library_copyright" , "sWidth" : "125px" }, - /* Encoded */ { "sTitle" : "Encoded By" , "mDataProp" : "encoded_by" , "bVisible" : false , "sClass" : "library_encoded" , "sWidth" : "150px" }, - /* Genre */ { "sTitle" : "Genre" , "mDataProp" : "genre" , "bVisible" : false , "sClass" : "library_genre" , "sWidth" : "100px" }, - /* ISRC Number */ { "sTitle" : "ISRC" , "mDataProp" : "isrc_number" , "bVisible" : false , "sClass" : "library_isrc" , "sWidth" : "150px" }, - /* Label */ { "sTitle" : "Label" , "mDataProp" : "label" , "bVisible" : false , "sClass" : "library_label" , "sWidth" : "125px" }, - /* Language */ { "sTitle" : "Language" , "mDataProp" : "language" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" }, + /* ftype */ { "sTitle" : "" , "mDataProp" : "ftype" , "bSearchable" : false , "bVisible" : false } , + /* 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" : 0 } , + /* Title */ { "sTitle" : "Title" , "mDataProp" : "track_title" , "sClass" : "library_title" , "sWidth" : "170px" } , + /* Creator */ { "sTitle" : "Creator" , "mDataProp" : "artist_name" , "sClass" : "library_creator" , "sWidth" : "160px" } , + /* Album */ { "sTitle" : "Album" , "mDataProp" : "album_title" , "sClass" : "library_album" , "sWidth" : "150px" } , + /* Bit Rate */ { "sTitle" : "Bit Rate" , "mDataProp" : "bit_rate" , "bVisible" : false , "sClass" : "library_bitrate" , "sWidth" : "80px" }, + /* BPM */ { "sTitle" : "BPM" , "mDataProp" : "bpm" , "bVisible" : false , "sClass" : "library_bpm" , "sWidth" : "50px" }, + /* Composer */ { "sTitle" : "Composer" , "mDataProp" : "composer" , "bVisible" : false , "sClass" : "library_composer" , "sWidth" : "150px" }, + /* Conductor */ { "sTitle" : "Conductor" , "mDataProp" : "conductor" , "bVisible" : false , "sClass" : "library_conductor" , "sWidth" : "125px" }, + /* Copyright */ { "sTitle" : "Copyright" , "mDataProp" : "copyright" , "bVisible" : false , "sClass" : "library_copyright" , "sWidth" : "125px" }, + /* Encoded */ { "sTitle" : "Encoded By" , "mDataProp" : "encoded_by" , "bVisible" : false , "sClass" : "library_encoded" , "sWidth" : "150px" }, + /* Genre */ { "sTitle" : "Genre" , "mDataProp" : "genre" , "bVisible" : false , "sClass" : "library_genre" , "sWidth" : "100px" }, + /* ISRC Number */ { "sTitle" : "ISRC" , "mDataProp" : "isrc_number" , "bVisible" : false , "sClass" : "library_isrc" , "sWidth" : "150px" }, + /* Label */ { "sTitle" : "Label" , "mDataProp" : "label" , "bVisible" : false , "sClass" : "library_label" , "sWidth" : "125px" }, + /* Language */ { "sTitle" : "Language" , "mDataProp" : "language" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" }, /* Last Modified */ { "sTitle" : "Last Modified" , "mDataProp" : "mtime" , "bVisible" : false , "sClass" : "library_modified_time" , "sWidth" : "125px" }, - /* Last Played */ { "sTitle" : "Last Played " , "mDataProp" : "lptime" , "bVisible" : false , "sClass" : "library_modified_time" , "sWidth" : "125px" }, - /* Length */ { "sTitle" : "Length" , "mDataProp" : "length" , "sClass" : "library_length" , "sWidth" : "80px" } , - /* Mime */ { "sTitle" : "Mime" , "mDataProp" : "mime" , "bVisible" : false , "sClass" : "library_mime" , "sWidth" : "80px" }, - /* Mood */ { "sTitle" : "Mood" , "mDataProp" : "mood" , "bVisible" : false , "sClass" : "library_mood" , "sWidth" : "70px" }, - /* Owner */ { "sTitle" : "Owner" , "mDataProp" : "owner_id" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" }, - /* Replay Gain */ { "sTitle" : "Replay Gain" , "mDataProp" : "replay_gain" , "bVisible" : false , "sClass" : "library_replay_gain" , "sWidth" : "80px" }, - /* Sample Rate */ { "sTitle" : "Sample Rate" , "mDataProp" : "sample_rate" , "bVisible" : false , "sClass" : "library_sr" , "sWidth" : "80px" }, - /* Track Number */ { "sTitle" : "Track Number" , "mDataProp" : "track_number" , "bVisible" : false , "sClass" : "library_track" , "sWidth" : "65px" }, - /* Upload Time */ { "sTitle" : "Uploaded" , "mDataProp" : "utime" , "sClass" : "library_upload_time" , "sWidth" : "125px" } , - /* Website */ { "sTitle" : "Website" , "mDataProp" : "info_url" , "bVisible" : false , "sClass" : "library_url" , "sWidth" : "150px" }, - /* Year */ { "sTitle" : "Year" , "mDataProp" : "year" , "bVisible" : false , "sClass" : "library_year" , "sWidth" : "60px" } + /* Last Played */ { "sTitle" : "Last Played " , "mDataProp" : "lptime" , "bVisible" : false , "sClass" : "library_modified_time" , "sWidth" : "125px" }, + /* Length */ { "sTitle" : "Length" , "mDataProp" : "length" , "sClass" : "library_length" , "sWidth" : "80px" } , + /* Mime */ { "sTitle" : "Mime" , "mDataProp" : "mime" , "bVisible" : false , "sClass" : "library_mime" , "sWidth" : "80px" }, + /* Mood */ { "sTitle" : "Mood" , "mDataProp" : "mood" , "bVisible" : false , "sClass" : "library_mood" , "sWidth" : "70px" }, + /* Owner */ { "sTitle" : "Owner" , "mDataProp" : "owner_id" , "bVisible" : false , "sClass" : "library_language" , "sWidth" : "125px" }, + /* Replay Gain */ { "sTitle" : "Replay Gain" , "mDataProp" : "replay_gain" , "bVisible" : false , "sClass" : "library_replay_gain" , "sWidth" : "80px" }, + /* Sample Rate */ { "sTitle" : "Sample Rate" , "mDataProp" : "sample_rate" , "bVisible" : false , "sClass" : "library_sr" , "sWidth" : "80px" }, + /* Track Number */ { "sTitle" : "Track Number" , "mDataProp" : "track_number" , "bVisible" : false , "sClass" : "library_track" , "sWidth" : "65px" }, + /* Upload Time */ { "sTitle" : "Uploaded" , "mDataProp" : "utime" , "sClass" : "library_upload_time" , "sWidth" : "125px" } , + /* Website */ { "sTitle" : "Website" , "mDataProp" : "info_url" , "bVisible" : false , "sClass" : "library_url" , "sWidth" : "150px" }, + /* Year */ { "sTitle" : "Year" , "mDataProp" : "year" , "bVisible" : false , "sClass" : "library_year" , "sWidth" : "60px" } ], "bProcessing": true, @@ -472,7 +473,7 @@ var AIRTIME = (function(AIRTIME) { "bStateSave": true, "fnStateSaveParams": function (oSettings, oData) { - //remove oData components we don't want to save. + // remove oData components we don't want to save. delete oData.oSearch; delete oData.aoSearchCols; }, @@ -499,8 +500,8 @@ var AIRTIME = (function(AIRTIME) { length, a = oData.abVisCols; - //putting serialized data back into the correct js type to make - //sure everything works properly. + // putting serialized data back into the correct js type to make + // sure everything works properly. for (i = 0, length = a.length; i < length; i++) { if (typeof(a[i]) === "string") { a[i] = (a[i] === "true") ? true : false; @@ -524,11 +525,12 @@ var AIRTIME = (function(AIRTIME) { "sAjaxDataProp": "files", "fnServerData": function ( sSource, aoData, fnCallback ) { - /* The real validation check is done in dataTables.columnFilter.js - * We also need to check it here because datatable is redrawn everytime - * an action is performed in the Library page. - * In order for datatable to redraw the advanced search fields - * MUST all be valid. + /* + * The real validation check is done in + * dataTables.columnFilter.js We also need to check it here + * because datatable is redrawn everytime an action is performed + * in the Library page. In order for datatable to redraw the + * advanced search fields MUST all be valid. */ var advSearchFields = $("div#advanced_search").children(':visible'); var advSearchValid = validateAdvancedSearch(advSearchFields); @@ -536,11 +538,11 @@ var AIRTIME = (function(AIRTIME) { aoData.push( { name: "format", value: "json"} ); aoData.push( { name: "advSearch", value: advSearchValid} ); - //push whether to search files/playlists or all. + // 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": "POST", @@ -552,30 +554,39 @@ var AIRTIME = (function(AIRTIME) { "fnRowCallback": AIRTIME.library.fnRowCallback, "fnCreatedRow": function( nRow, aData, iDataIndex ) { - //add the play function to the library_type td + // add the play function to the library_type td $(nRow).find('td.library_type').click(function(){ if (aData.ftype === 'playlist' && aData.length !== '0.0'){ - playlistIndex = $(this).parent().attr('id').substring(3); //remove the pl_ + playlistIndex = $(this).parent().attr('id').substring(3); // remove + // the + // pl_ open_playlist_preview(playlistIndex, 0); } else if (aData.ftype === 'audioclip') { - open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name); + if (isAudioSupported(aData.mime)) { + open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name); + } } else if (aData.ftype == 'stream') { open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name); } else if (aData.ftype == 'block' && aData.bl_type == 'static') { - blockIndex = $(this).parent().attr('id').substring(3); //remove the bl_ + blockIndex = $(this).parent().attr('id').substring(3); // remove + // the + // bl_ open_block_preview(blockIndex, 0); } return false; }); alreadyclicked=false; - //call the context menu so we can prevent the event from propagating. + // call the context menu so we can prevent the event from + // propagating. $(nRow).find('td:not(.library_checkbox, .library_type)').click(function(e){ var el=$(this); if (alreadyclicked) { alreadyclicked=false; // reset - clearTimeout(alreadyclickedTimeout); // prevent this from happening + clearTimeout(alreadyclickedTimeout); // prevent this + // from + // happening // do what needs to happen on double click. $tr = $(el).parent(); @@ -587,8 +598,8 @@ var AIRTIME = (function(AIRTIME) { alreadyclicked=true; alreadyclickedTimeout=setTimeout(function(){ alreadyclicked=false; // reset when it happens - // do what needs to happen on single click. - // use el instead of $(this) because $(this) is + // do what needs to happen on single click. + // use el instead of $(this) because $(this) is // no longer the element el.contextMenu({x: e.pageX, y: e.pageY}); },300); // <-- dblclick tolerance here @@ -596,7 +607,8 @@ var AIRTIME = (function(AIRTIME) { return false; }); - //add a tool tip to appear when the user clicks on the type icon. + // add a tool tip to appear when the user clicks on the type + // icon. $(nRow).find("td:not(.library_checkbox, .library_type)").qtip({ content: { text: "Loading...", @@ -620,7 +632,8 @@ var AIRTIME = (function(AIRTIME) { }, my: 'left center', at: 'right center', - viewport: $(window), // Keep the tooltip on-screen at all times + viewport: $(window), // Keep the tooltip on-screen at + // all times effect: false // Disable positioning animation }, style: { @@ -638,10 +651,11 @@ var AIRTIME = (function(AIRTIME) { hide: {event:'mouseout', delay: 50, fixed:true} }); }, - //remove any selected nodes before the draw. + // remove any selected nodes before the draw. "fnPreDrawCallback": function( oSettings ) { - //make sure any dragging helpers are removed or else they'll be stranded on the screen. + // make sure any dragging helpers are removed or else they'll be + // stranded on the screen. $("#draggingContainer").remove(); }, "fnDrawCallback": AIRTIME.library.fnDrawCallback, @@ -673,14 +687,32 @@ var AIRTIME = (function(AIRTIME) { setColumnFilter(oTable); oTable.fnSetFilteringDelay(350); - + + var simpleSearchText; + $libContent.on("click", "legend", function(){ + $simpleSearch = $libContent.find("#library_display_filter label"); var $fs = $(this).parents("fieldset"); if ($fs.hasClass("closed")) { $fs.removeClass("closed"); + + //keep value of simple search for when user switches back to it + simpleSearchText = $simpleSearch.find('input').val(); + + // clear the simple search text field and reset datatable + $(".dataTables_filter input").val("").keyup(); + + $simpleSearch.addClass("sp-invisible"); } else { + //clear the advanced search fields and reset datatable + $(".filter_column input").val("").keyup(); + + //reset datatable with previous simple search results (if any) + $(".dataTables_filter input").val(simpleSearchText).keyup(); + + $simpleSearch.removeClass("sp-invisible"); $fs.addClass("closed"); } }); @@ -734,7 +766,7 @@ var AIRTIME = (function(AIRTIME) { addQtipToSCIcons(); - //begin context menu initialization. + // begin context menu initialization. $.contextMenu({ selector: '#library_display td:not(.library_checkbox)', trigger: "left", @@ -749,7 +781,7 @@ var AIRTIME = (function(AIRTIME) { function processMenuItems(oItems) { - //define an add to playlist callback. + // define an add to playlist callback. if (oItems.pl_add !== undefined) { var aItems = []; @@ -761,7 +793,7 @@ var AIRTIME = (function(AIRTIME) { oItems.pl_add.callback = callback; } - //define an edit callback. + // define an edit callback. if (oItems.edit !== undefined) { if (data.ftype === "audioclip") { @@ -785,7 +817,7 @@ var AIRTIME = (function(AIRTIME) { oItems.edit.callback = callback; } - //define a play callback. + // define a play callback. if (oItems.play !== undefined) { if (oItems.play.mime !== undefined) { @@ -796,23 +828,28 @@ var AIRTIME = (function(AIRTIME) { callback = function() { if (data.ftype === 'playlist' && data.length !== '0.0'){ - playlistIndex = $(this).parent().attr('id').substring(3); //remove the pl_ + playlistIndex = $(this).parent().attr('id').substring(3); // remove + // the + // pl_ open_playlist_preview(playlistIndex, 0); } else if (data.ftype === 'audioclip' || data.ftype === 'stream') { open_audio_preview(data.ftype, data.audioFile, data.track_title, data.artist_name); } else if (data.ftype === 'block') { - blockIndex = $(this).parent().attr('id').substring(3); //remove the pl_ + blockIndex = $(this).parent().attr('id').substring(3); // remove + // the + // pl_ open_block_preview(blockIndex, 0); } }; oItems.play.callback = callback; } - //define a delete callback. + // define a delete callback. if (oItems.del !== undefined) { - //delete through the playlist controller, will reset - //playlist screen if this is the currently edited playlist. + // delete through the playlist controller, will reset + // playlist screen if this is the currently edited + // playlist. if ((data.ftype === "playlist" || data.ftype === "block") && screen === "playlist") { callback = function() { aMedia = []; @@ -846,7 +883,7 @@ var AIRTIME = (function(AIRTIME) { oItems.del.callback = callback; } - //define a download callback. + // define a download callback. if (oItems.download !== undefined) { callback = function() { @@ -854,11 +891,11 @@ var AIRTIME = (function(AIRTIME) { }; oItems.download.callback = callback; } - //add callbacks for Soundcloud menu items. + // add callbacks for Soundcloud menu items. if (oItems.soundcloud !== undefined) { var soundcloud = oItems.soundcloud.items; - //define an upload to soundcloud callback. + // define an upload to soundcloud callback. if (soundcloud.upload !== undefined) { callback = function() { @@ -869,7 +906,7 @@ var AIRTIME = (function(AIRTIME) { soundcloud.upload.callback = callback; } - //define a view on soundcloud callback + // define a view on soundcloud callback if (soundcloud.view !== undefined) { callback = function() { @@ -985,7 +1022,8 @@ function addQtipToSCIcons(){ viewport: $(window) }, show: { - ready: true // Needed to make it show on first mouseover event + ready: true // Needed to make it show on first mouseover + // event } }); } @@ -1012,7 +1050,8 @@ function addQtipToSCIcons(){ viewport: $(window) }, show: { - ready: true // Needed to make it show on first mouseover event + ready: true // Needed to make it show on first mouseover + // event } }); }else if($(this).hasClass("sc-error")){ @@ -1039,7 +1078,8 @@ function addQtipToSCIcons(){ viewport: $(window) }, show: { - ready: true // Needed to make it show on first mouseover event + ready: true // Needed to make it show on first mouseover + // event } }); } @@ -1093,7 +1133,7 @@ function validateAdvancedSearch(divs) { } } - //string fields do not need validation + // string fields do not need validation if (searchTermType !== "s") { valid = regExpr.test(searchTerm[i]); if (!valid) allValid = false; @@ -1101,11 +1141,11 @@ function validateAdvancedSearch(divs) { addRemoveValidationIcons(valid, $(field), searchTermType); - /* Empty fields should not have valid/invalid indicator - * Range values are considered valid even if only the - * 'From' value is provided. Therefore, if the 'To' value - * is empty but the 'From' value is not empty we need to - * keep the validation icon on screen. + /* + * Empty fields should not have valid/invalid indicator Range values + * are considered valid even if only the 'From' value is provided. + * Therefore, if the 'To' value is empty but the 'From' value is not + * empty we need to keep the validation icon on screen. */ } else if (searchTerm[0] === "" && searchTerm[1] !== "" || searchTerm[0] === "" && searchTerm[1] === ""){ @@ -1141,7 +1181,7 @@ function addRemoveValidationIcons(valid, field, searchTermType) { if (valid) { if (!field.closest('div').children(':last-child').hasClass('checked-icon')) { - //remove invalid icon before adding valid icon + // remove invalid icon before adding valid icon if (field.closest('div').children(':last-child').hasClass('not-available-icon')) { field.closest('div').children(':last-child').remove(); } @@ -1149,7 +1189,7 @@ function addRemoveValidationIcons(valid, field, searchTermType) { } } else { if (!field.closest('div').children(':last-child').hasClass('not-available-icon')) { - //remove valid icon before adding invalid icon + // remove valid icon before adding invalid icon if (field.closest('div').children(':last-child').hasClass('checked-icon')) { field.closest('div').children(':last-child').remove(); } @@ -1158,12 +1198,9 @@ function addRemoveValidationIcons(valid, field, searchTermType) { } } -/* Validation types: - * s => string - * i => integer - * n => numeric (positive/negative, whole/decimals) - * t => timestamp - * l => length +/* + * Validation types: s => string i => integer n => numeric (positive/negative, + * whole/decimals) t => timestamp l => length */ var validationTypes = { "album_title" : "s", @@ -1192,7 +1229,7 @@ var validationTypes = { "owner_id" : "s", "rating" : "i", "replay_gain" : "n", - "sample_rate" : "i", + "sample_rate" : "n", "track_title" : "s", "track_number" : "i", "info_url" : "s", diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index e79678201..1b5e765a4 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -369,29 +369,33 @@ var AIRTIME = (function(AIRTIME){ $.each($("div .big_play"), function(index, value){ if ($(value).attr('blockId') === undefined) { var mime = $(value).attr("data-mime-type"); - if (isAudioSupported(mime)) { - $(value).bind("click", openAudioPreview); - } else { - $(value).attr("class", "big_play_disabled dark_class"); - $(value).qtip({ - content: 'Your browser does not support playing this file type: "'+ mime +'"', - show: 'mouseover', - hide: { - delay: 500, - fixed: true - }, - style: { - border: { - width: 0, - radius: 4 + //If mime is undefined it is likely because the file was + //deleted from the library. This case is handled in mod.onReady() + if (mime !== undefined) { + if (isAudioSupported(mime)) { + $(value).bind("click", openAudioPreview); + } else { + $(value).attr("class", "big_play_disabled dark_class"); + $(value).qtip({ + content: 'Your browser does not support playing this file type: "'+ mime +'"', + show: 'mouseover', + hide: { + delay: 500, + fixed: true }, - classes: "ui-tooltip-dark ui-tooltip-rounded" - }, - position: { - my: "left bottom", - at: "right center" - }, - }) + style: { + border: { + width: 0, + radius: 4 + }, + classes: "ui-tooltip-dark ui-tooltip-rounded" + }, + position: { + my: "left bottom", + at: "right center" + }, + }) + } } } else { if ($(value).attr('blocktype') === 'dynamic') { diff --git a/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js b/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js index 0dc3bc0a9..c33ccc351 100644 --- a/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js +++ b/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js @@ -8,14 +8,17 @@ function setSmartBlockEvents() { /********** ADD CRITERIA ROW **********/ form.find('#criteria_add').live('click', function(){ - var div = $('dd[id="sp_criteria-element"]').children('div:visible:last').next(); - - div.show(); + var div = $('dd[id="sp_criteria-element"]').children('div:visible:last'); + + div.find('.db-logic-label').text('and').show(); + div = div.next().show(); + div.children().removeAttr('disabled'); div = div.next(); if (div.length === 0) { $(this).hide(); } + appendAddButton(); appendModAddButton(); removeButtonCheck(); @@ -24,7 +27,7 @@ function setSmartBlockEvents() { /********** ADD MODIFIER ROW **********/ form.find('a[id^="modifier_add"]').live('click', function(){ var criteria_value = $(this).siblings('select[name^="sp_criteria_field"]').val(); - + //make new modifier row var newRow = $(this).parent().clone(), newRowCrit = newRow.find('select[name^="sp_criteria_field"]'), @@ -285,6 +288,11 @@ function reindexElements() { var divs = $('#smart-block-form').find('div select[name^="sp_criteria_field"]').parent(), index = 0, modIndex = 0; + /* Hide all logic labels + * We will re-add them as each row gets indexed + */ + $('.db-logic-label').text('').hide(); + $.each(divs, function(i, div){ if (i > 0 && index < 26) { @@ -292,8 +300,14 @@ function reindexElements() { * a modifier row */ if ($(div).find('select[name^="sp_criteria_field"]').hasClass('sp-invisible')) { + if ($(div).is(':visible')) { + $(div).prev().find('.db-logic-label').text('or').show(); + } modIndex++; } else { + if ($(div).is(':visible')) { + $(div).prev().find('.db-logic-label').text('and').show(); + } index++; modIndex = 0; } @@ -337,8 +351,8 @@ function setupUI() { */ var plContents = $('#spl_sortable').children(); var shuffleButton = $('button[id="shuffle_button"]'); - - if (plContents.text() !== 'Empty playlist') { + + if (!plContents.hasClass('spl_empty')) { if (shuffleButton.hasClass('ui-state-disabled')) { shuffleButton.removeClass('ui-state-disabled'); shuffleButton.removeAttr('disabled'); @@ -384,6 +398,28 @@ function setupUI() { at: "right center" }, }); + + $(".repeat_tracks_help_icon").qtip({ + content: { + text: "If your criteria is too strict, Airtime may not be able to fill up the desired smart block length." + + " Hence, if you check this option, tracks will be used more than once." + }, + hide: { + delay: 500, + fixed: true + }, + style: { + border: { + width: 0, + radius: 4 + }, + classes: "ui-tooltip-dark ui-tooltip-rounded" + }, + position: { + my: "left bottom", + at: "right center" + }, + }); } function enableAndShowExtraField(valEle, index) { diff --git a/airtime_mvc/public/js/airtime/showbuilder/builder.js b/airtime_mvc/public/js/airtime/showbuilder/builder.js index fc8463481..48015f92c 100644 --- a/airtime_mvc/public/js/airtime/showbuilder/builder.js +++ b/airtime_mvc/public/js/airtime/showbuilder/builder.js @@ -339,11 +339,23 @@ var AIRTIME = (function(AIRTIME){ }); }; + mod.jumpToCurrentTrack = function() { + var $scroll = $sbContent.find(".dataTables_scrolling"); + var scrolled = $scroll.scrollTop(); + var scrollingTop = $scroll.offset().top; + var oTable = $('#show_builder_table').dataTable(); + var current = $sbTable.find("."+NOW_PLAYING_CLASS); + var currentTop = current.offset().top; + + $scroll.scrollTop(currentTop - scrollingTop + scrolled); + } + mod.builderDataTable = function() { $sbContent = $('#show_builder'); $lib = $("#library_content"), $sbTable = $sbContent.find('table'); - + var isInitialized = false; + oSchedTable = $sbTable.dataTable( { "aoColumns": [ /* checkbox */ {"mDataProp": "allowed", "sTitle": "", "sWidth": "15px", "sClass": "sb-checkbox"}, @@ -357,7 +369,8 @@ var AIRTIME = (function(AIRTIME){ /* cue in */ {"mDataProp": "cuein", "sTitle": "Cue In", "bVisible": false, "sClass": "sb-cue-in"}, /* cue out */ {"mDataProp": "cueout", "sTitle": "Cue Out", "bVisible": false, "sClass": "sb-cue-out"}, /* fade in */ {"mDataProp": "fadein", "sTitle": "Fade In", "bVisible": false, "sClass": "sb-fade-in"}, - /* fade out */ {"mDataProp": "fadeout", "sTitle": "Fade Out", "bVisible": false, "sClass": "sb-fade-out"} + /* fade out */ {"mDataProp": "fadeout", "sTitle": "Fade Out", "bVisible": false, "sClass": "sb-fade-out"}, + /* Mime */ {"mDataProp" : "mime", "sTitle" : "Mime", "bVisible": false, "sClass": "sb-mime"} ], "bJQueryUI": true, @@ -537,11 +550,16 @@ var AIRTIME = (function(AIRTIME){ $image = $nRow.find('td.sb-image'); //check if the file exists. if (aData.image === true) { - $image.html('') + $nRow.addClass("lib-audio"); + if (!isAudioSupported(aData.mime)) { + $image.html(''); + } else { + $image.html('') .click(function() { open_show_preview(aData.instance, aData.pos); return false; }); + } } else { $image.html(''); @@ -636,6 +654,17 @@ var AIRTIME = (function(AIRTIME){ $("#draggingContainer").remove(); }, "fnDrawCallback": function fnBuilderDrawCallback(oSettings, json) { + var isInitialized = false; + + if (!isInitialized) { + //when coming to 'Now Playing' page we want the page + //to jump to the current track + if ($(this).find("."+NOW_PLAYING_CLASS).length > 0) { + mod.jumpToCurrentTrack(); + } + } + + isInitialized = true; var wrapperDiv, markerDiv, $td, @@ -966,13 +995,18 @@ var AIRTIME = (function(AIRTIME){ "") .append("
    " + "
    ") - .append("
    " + + "
    "); + + //if 'Add/Remove content' was chosen from the context menu + //in the Calendar do not append these buttons + if ($(".ui-dialog-content").length === 0) { + $menu.append("
    " + "
    ") .append("
    " + "
    "); + } $toolbar.append($menu); $menu = undefined; @@ -1021,7 +1055,7 @@ var AIRTIME = (function(AIRTIME){ if (AIRTIME.button.isDisabled('icon-step-forward', true) === true) { return; } - + /* var $scroll = $sbContent.find(".dataTables_scrolling"), scrolled = $scroll.scrollTop(), scrollingTop = $scroll.offset().top, @@ -1029,6 +1063,8 @@ var AIRTIME = (function(AIRTIME){ currentTop = current.offset().top; $scroll.scrollTop(currentTop - scrollingTop + scrolled); + */ + mod.jumpToCurrentTrack(); }); //delete overbooked tracks. @@ -1196,7 +1232,7 @@ var AIRTIME = (function(AIRTIME){ }; } - }); + }); }; return AIRTIME; diff --git a/airtime_mvc/public/js/airtime/showbuilder/main_builder.js b/airtime_mvc/public/js/airtime/showbuilder/main_builder.js index b640a2882..48e503ac6 100644 --- a/airtime_mvc/public/js/airtime/showbuilder/main_builder.js +++ b/airtime_mvc/public/js/airtime/showbuilder/main_builder.js @@ -113,174 +113,180 @@ AIRTIME = (function(AIRTIME) { } mod.onReady = function() { - //define module vars. - $lib = $("#library_content"); - $builder = $("#show_builder"); - $fs = $builder.find('fieldset'); - - /* - * Icon hover states for search. - */ - $builder.on("mouseenter", ".sb-timerange .ui-button", function(ev) { - $(this).addClass("ui-state-hover"); - }); - $builder.on("mouseleave", ".sb-timerange .ui-button", function(ev) { - $(this).removeClass("ui-state-hover"); - }); - - $builder.find(dateStartId).datepicker(oBaseDatePickerSettings); - $builder.find(timeStartId).timepicker(oBaseTimePickerSettings); - $builder.find(dateEndId).datepicker(oBaseDatePickerSettings); - $builder.find(timeEndId).timepicker(oBaseTimePickerSettings); - - oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, dateEndId, timeEndId); - AIRTIME.showbuilder.fnServerData.start = oRange.start; - AIRTIME.showbuilder.fnServerData.end = oRange.end; + // define module vars. + $lib = $("#library_content"); + $builder = $("#show_builder"); + $fs = $builder.find('fieldset'); - AIRTIME.library.libraryInit(); - AIRTIME.showbuilder.builderDataTable(); - setWidgetSize(); - - $libWrapper = $lib.find("#library_display_wrapper"); - $libWrapper.prepend($libClose); - - $builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 95); - - $builder.on("click", "#sb_submit", showSearchSubmit); + /* + * Icon hover states for search. + */ + $builder.on("mouseenter", ".sb-timerange .ui-button", function(ev) { + $(this).addClass("ui-state-hover"); + }); + $builder.on("mouseleave", ".sb-timerange .ui-button", function(ev) { + $(this).removeClass("ui-state-hover"); + }); - $builder.on("click","#sb_edit", function (ev){ - var schedTable = $("#show_builder_table").dataTable(); - - //reset timestamp to redraw the cursors. - AIRTIME.showbuilder.resetTimestamp(); - - $lib.show() - .width(Math.floor(screenWidth * 0.48)); - - $builder.width(Math.floor(screenWidth * 0.48)) - .find("#sb_edit") - .remove() - .end() - .find("#sb_date_start") - .css("margin-left", 0) - .end(); - - schedTable.fnDraw(); - - $.ajax({ - url: "/usersettings/set-now-playing-screen-settings", - type: "POST", - data: {settings : {library : true}, format: "json"}, - dataType: "json", - success: function(){} - }); - }); - - $lib.on("click", "#sb_lib_close", function() { - var schedTable = $("#show_builder_table").dataTable(); + $builder.find(dateStartId).datepicker(oBaseDatePickerSettings); + $builder.find(timeStartId).timepicker(oBaseTimePickerSettings); + $builder.find(dateEndId).datepicker(oBaseDatePickerSettings); + $builder.find(timeEndId).timepicker(oBaseTimePickerSettings); + + oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, + dateEndId, timeEndId); + AIRTIME.showbuilder.fnServerData.start = oRange.start; + AIRTIME.showbuilder.fnServerData.end = oRange.end; + + AIRTIME.library.libraryInit(); + AIRTIME.showbuilder.builderDataTable(); + setWidgetSize(); + + $libWrapper = $lib.find("#library_display_wrapper"); + $libWrapper.prepend($libClose); + + $builder.find('.dataTables_scrolling').css("max-height", + widgetHeight - 95); + + $builder.on("click", "#sb_submit", showSearchSubmit); + + $builder.on("click", "#sb_edit", function(ev) { + var schedTable = $("#show_builder_table").dataTable(); + + // reset timestamp to redraw the cursors. + AIRTIME.showbuilder.resetTimestamp(); + + $lib.show().width(Math.floor(screenWidth * 0.48)); + + $builder.width(Math.floor(screenWidth * 0.48)).find("#sb_edit") + .remove().end().find("#sb_date_start") + .css("margin-left", 0).end(); + + schedTable.fnDraw(); + + $.ajax( { + url : "/usersettings/set-now-playing-screen-settings", + type : "POST", + data : { + settings : { + library : true + }, + format : "json" + }, + dataType : "json", + success : function() { + } + }); + }); + + $lib.on("click", "#sb_lib_close", function() { + var schedTable = $("#show_builder_table").dataTable(); + + $lib.hide(); + $builder.width(screenWidth).find(".sb-timerange").prepend( + $toggleLib).find("#sb_date_start").css("margin-left", 30) + .end().end(); + + $toggleLib.removeClass("ui-state-hover"); + schedTable.fnDraw(); + + $.ajax( { + url : "/usersettings/set-now-playing-screen-settings", + type : "POST", + data : { + settings : { + library : false + }, + format : "json" + }, + dataType : "json", + success : function() { + } + }); + }); + + $builder.find('legend').click( + function(ev, item) { + + if ($fs.hasClass("closed")) { + + $fs.removeClass("closed"); + $builder.find('.dataTables_scrolling').css( + "max-height", widgetHeight - 150); + } else { + $fs.addClass("closed"); + + // set defaults for the options. + $fs.find('select').val(0); + $fs.find('input[type="checkbox"]').attr("checked", + false); + $builder.find('.dataTables_scrolling').css( + "max-height", widgetHeight - 110); + } + }); + + // set click event for all my shows checkbox. + $builder.on("click", "#sb_my_shows", function(ev) { + + if ($(this).is(':checked')) { + $(ev.delegateTarget).find('#sb_show_filter').val(0); + } + + showSearchSubmit(); + }); + + //set select event for choosing a show. + $builder.on("change", '#sb_show_filter', function(ev) { + + if ($(this).val() !== 0) { + $(ev.delegateTarget).find('#sb_my_shows') + .attr("checked", false); + } + + showSearchSubmit(); + + }); + + function checkScheduleUpdates() { + var data = {}, oTable = $('#show_builder_table').dataTable(), fn = oTable + .fnSettings().fnServerData, start = fn.start, end = fn.end; + + data["format"] = "json"; + data["start"] = start; + data["end"] = end; + data["timestamp"] = AIRTIME.showbuilder.getTimestamp(); + data["instances"] = AIRTIME.showbuilder.getShowInstances(); + + if (fn.hasOwnProperty("ops")) { + data["myShows"] = fn.ops.myShows; + data["showFilter"] = fn.ops.showFilter; + } + + $.ajax( { + "dataType" : "json", + "type" : "GET", + "url" : "/showbuilder/check-builder-feed", + "data" : data, + "success" : function(json) { + if (json.update === true) { + oTable.fnDraw(); + } + } + }); + } + + //check if the timeline view needs updating. + setInterval(checkScheduleUpdates, 5 * 1000); //need refresh in milliseconds + }; + + mod.onResize = function() { + + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(setWidgetSize, 100); + }; + + return AIRTIME; - $lib.hide(); - $builder.width(screenWidth) - .find(".sb-timerange") - .prepend($toggleLib) - .find("#sb_date_start") - .css("margin-left", 30) - .end() - .end(); - - $toggleLib.removeClass("ui-state-hover"); - schedTable.fnDraw(); - - $.ajax({ - url: "/usersettings/set-now-playing-screen-settings", - type: "POST", - data: {settings : {library : false}, format: "json"}, - dataType: "json", - success: function(){} - }); - }); - - $builder.find('legend').click(function(ev, item){ - - if ($fs.hasClass("closed")) { - - $fs.removeClass("closed"); - $builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 150); - } - else { - $fs.addClass("closed"); - - //set defaults for the options. - $fs.find('select').val(0); - $fs.find('input[type="checkbox"]').attr("checked", false); - $builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 110); - } - }); - - //set click event for all my shows checkbox. - $builder.on("click", "#sb_my_shows", function(ev) { - - if ($(this).is(':checked')) { - $(ev.delegateTarget).find('#sb_show_filter').val(0); - } - - showSearchSubmit(); - }); - - //set select event for choosing a show. - $builder.on("change", '#sb_show_filter', function(ev) { - - if ($(this).val() !== 0) { - $(ev.delegateTarget).find('#sb_my_shows').attr("checked", false); - } - - showSearchSubmit(); - - }); - - function checkScheduleUpdates(){ - var data = {}, - oTable = $('#show_builder_table').dataTable(), - fn = oTable.fnSettings().fnServerData, - start = fn.start, - end = fn.end; - - data["format"] = "json"; - data["start"] = start; - data["end"] = end; - data["timestamp"] = AIRTIME.showbuilder.getTimestamp(); - data["instances"] = AIRTIME.showbuilder.getShowInstances(); - - if (fn.hasOwnProperty("ops")) { - data["myShows"] = fn.ops.myShows; - data["showFilter"] = fn.ops.showFilter; - } - - $.ajax( { - "dataType": "json", - "type": "GET", - "url": "/showbuilder/check-builder-feed", - "data": data, - "success": function(json) { - if (json.update === true) { - oTable.fnDraw(); - } - } - } ); - } - - //check if the timeline view needs updating. - setInterval(checkScheduleUpdates, 5 * 1000); //need refresh in milliseconds - }; - - mod.onResize = function() { - - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(setWidgetSize, 100); - }; - - return AIRTIME; - } (AIRTIME || {})); $(document).ready(AIRTIME.builderMain.onReady); diff --git a/airtime_mvc/public/js/bootstrap/bootstrap.js b/airtime_mvc/public/js/bootstrap/bootstrap.js index 7f303eb88..411ca794f 100644 --- a/airtime_mvc/public/js/bootstrap/bootstrap.js +++ b/airtime_mvc/public/js/bootstrap/bootstrap.js @@ -574,7 +574,7 @@ }) }(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.1.0 + * bootstrap-dropdown.js v2.2.1 * http://twitter.github.com/bootstrap/javascript.html#dropdowns * ============================================================ * Copyright 2012 Twitter, Inc. @@ -675,8 +675,9 @@ } function clearMenus() { - getParent($(toggle)) - .removeClass('open') + $(toggle).each(function () { + getParent($(this)).removeClass('open') + }) } function getParent($this) { @@ -685,7 +686,7 @@ if (!selector) { selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 } $parent = $(selector) @@ -713,14 +714,15 @@ /* APPLY TO STANDARD DROPDOWN ELEMENTS * =================================== */ - $(function () { - $('html') - .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) - $('body') - .on('click.dropdown touchstart.dropdown.data-api', '.dropdown', function (e) { e.stopPropagation() }) - .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) - .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) - }) + $(document) + + // menu options don't work on tablet so trying this hack for now: + // https://github.com/twitter/bootstrap/issues/4550 + //.on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) + .on('click.dropdown.data-api', clearMenus) + .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) }(window.jQuery);/* ========================================================= * bootstrap-modal.js v2.1.0 diff --git a/airtime_mvc/public/js/datatables/plugin/dataTables.columnFilter.js b/airtime_mvc/public/js/datatables/plugin/dataTables.columnFilter.js index 4084fbbdb..80cb011cf 100644 --- a/airtime_mvc/public/js/datatables/plugin/dataTables.columnFilter.js +++ b/airtime_mvc/public/js/datatables/plugin/dataTables.columnFilter.js @@ -190,7 +190,7 @@ } else if (th.attr('id') == "length") { label = " hh:mm:ss.t"; } else if (th.attr('id') == "sample_rate") { - label = " Hz"; + label = " kHz"; } th.html(_fnRangeLabelPart(0)); diff --git a/airtime_mvc/tests/phpunit.xml b/airtime_mvc/tests/phpunit.xml index e69de29bb..01ad1609e 100644 --- a/airtime_mvc/tests/phpunit.xml +++ b/airtime_mvc/tests/phpunit.xml @@ -0,0 +1,2 @@ + + diff --git a/changelog b/changelog index 115265768..123d71e5f 100644 --- a/changelog +++ b/changelog @@ -67,7 +67,7 @@ * Removing a watched directory and adding it again preserves playlists & shows with those files. * An icon in the playlist shows whether a file is missing on disk, warning the user that the playlist will not go according to plan. * Media monitor detects add and removal of watched temporary local storage (USB disks for example) and network drives. - * Broadcast Log - export play count of tracks within a given time range.  Useful for royalty reporting purposes. + * Broadcast Log - export play count of tracks within a given time range. Useful for royalty reporting purposes. * Minor Improvements: * Ability to turn off the broadcast. * Editing metadata in the library will update the metadata on disk. @@ -78,7 +78,7 @@ * Repeating shows default to "No End" * Ability to "View on Soundcloud" for recorded shows in the calendar * "Listen" preview player no longer falls behind the broadcast (you can only mute the stream now, not stop it) - * Tracks that cannot be played will be rejected on upload and put in to the directory "/srv/airtime/store/problem_files" (but currently it will not tell you that it rejected them - sorry\!) + * Tracks that cannot be played will be rejected on upload and put in to the directory "/srv/airtime/stor/problem_files" (but currently it will not tell you that it rejected them - sorry\!) * Library is automatically refreshed when media import is finished * Show "Disk Full" message when trying to upload a file that wont fit on the disk * Reduced CPU utilization for OGG streams diff --git a/dev_tools/fabric/fab_liquidsoap_compile.py b/dev_tools/fabric/fab_liquidsoap_compile.py index 221aa41c6..98f816e47 100644 --- a/dev_tools/fabric/fab_liquidsoap_compile.py +++ b/dev_tools/fabric/fab_liquidsoap_compile.py @@ -181,7 +181,8 @@ libmad-ocaml-dev libtaglib-ocaml-dev libalsa-ocaml-dev libtaglib-ocaml-dev libvo libspeex-dev libspeexdsp-dev speex libladspa-ocaml-dev festival festival-dev \ libsamplerate-dev libxmlplaylist-ocaml-dev libxmlrpc-light-ocaml-dev libflac-dev \ libxml-dom-perl libxml-dom-xpath-perl patch autoconf libmp3lame-dev \ -libcamomile-ocaml-dev libcamlimages-ocaml-dev libtool libpulse-dev libjack-dev camlidl libfaad-dev''') +libcamomile-ocaml-dev libcamlimages-ocaml-dev libtool libpulse-dev libjack-dev +camlidl libfaad-dev libpcre-ocaml-dev''') root = '/home/martin/src' do_run('mkdir -p %s' % root) diff --git a/install_full/ubuntu/airtime-full-install b/install_full/ubuntu/airtime-full-install index 11bff85ec..955ec3ac7 100755 --- a/install_full/ubuntu/airtime-full-install +++ b/install_full/ubuntu/airtime-full-install @@ -27,11 +27,11 @@ code=`lsb_release -cs` if [ "$dist" = "Debian" ]; then set +e - grep -E "deb +http://www.deb-multimedia.org/? squeeze +main +non-free" /etc/apt/sources.list + grep -E "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list returncode=$? set -e if [ "$returncode" -ne "0" ]; then - echo "deb http://www.deb-multimedia.org squeeze main non-free" >> /etc/apt/sources.list + echo "deb http://backports.debian.org/debian-backports squeeze-backports main" >> /etc/apt/sources.list fi fi diff --git a/install_full/ubuntu/airtime-full-install-nginx b/install_full/ubuntu/airtime-full-install-nginx index 6b802b3bb..7e38e34cb 100755 --- a/install_full/ubuntu/airtime-full-install-nginx +++ b/install_full/ubuntu/airtime-full-install-nginx @@ -29,9 +29,9 @@ dist=`lsb_release -is` code=`lsb_release -cs` if [ "$dist" -eq "Debian" ]; then - grep "deb http://www.deb-multimedia.org squeeze main non-free" /etc/apt/sources.list + grep "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list if [ "$?" -ne "0" ]; then - echo "deb http://www.deb-multimedia.org squeeze main non-free" >> /etc/apt/sources.list + echo "deb http://backports.debian.org/debian-backports squeeze-backports main" >> /etc/apt/sources.list fi fi diff --git a/install_minimal/upgrades/airtime-2.1.0/common/Version20120405114454.php b/install_minimal/upgrades/airtime-2.1.0/common/Version20120405114454.php index 1f17976d0..073457cbb 100644 --- a/install_minimal/upgrades/airtime-2.1.0/common/Version20120405114454.php +++ b/install_minimal/upgrades/airtime-2.1.0/common/Version20120405114454.php @@ -9,16 +9,16 @@ class Version20120405114454 extends AbstractMigration { public function up(Schema $schema) { - //create cc_subjs_token table - $cc_subjs_token = $schema->createTable('cc_subjs_token'); - + //create cc_subjs_token table + $cc_subjs_token = $schema->createTable('cc_subjs_token'); + $cc_subjs_token->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => true)); - $cc_subjs_token->addColumn('user_id', 'integer', array('notnull' => 1)); - $cc_subjs_token->addColumn('action', 'string', array('length' => 255, 'notnull' => 1)); + $cc_subjs_token->addColumn('user_id', 'integer', array('notnull' => 1)); + $cc_subjs_token->addColumn('action', 'string', array('length' => 255, 'notnull' => 1)); $cc_subjs_token->addColumn('token', 'string', array('length' => 40, 'notnull' => 1)); - $cc_subjs_token->addColumn('created', 'datetime', array('notnull' => 1)); - - $cc_subjs_token->setPrimaryKey(array('id')); + $cc_subjs_token->addColumn('created', 'datetime', array('notnull' => 1)); + + $cc_subjs_token->setPrimaryKey(array('id')); //end create cc_subjs_token table } diff --git a/install_minimal/upgrades/airtime-2.1.0/common/Version20120410104441.php b/install_minimal/upgrades/airtime-2.1.0/common/Version20120410104441.php index aada5793c..80db8b1b3 100644 --- a/install_minimal/upgrades/airtime-2.1.0/common/Version20120410104441.php +++ b/install_minimal/upgrades/airtime-2.1.0/common/Version20120410104441.php @@ -24,21 +24,21 @@ class Version20120410104441 extends AbstractMigration $this->_addSql("ALTER TABLE cc_files ADD temp_br integer"); $this->_addSql("ALTER TABLE cc_files ADD temp_sr integer"); - $this->_addSql("UPDATE cc_files SET temp_br = bit_rate::integer"); - $this->_addSql("UPDATE cc_files SET temp_sr = sample_rate::integer"); + $this->_addSql("UPDATE cc_files SET temp_br = bit_rate::integer"); + $this->_addSql("UPDATE cc_files SET temp_sr = sample_rate::integer"); - $this->_addSql("ALTER TABLE cc_files DROP COLUMN sample_rate"); + $this->_addSql("ALTER TABLE cc_files DROP COLUMN sample_rate"); $this->_addSql("ALTER TABLE cc_files DROP COLUMN bit_rate"); - - $this->_addSql("ALTER TABLE cc_files RENAME COLUMN temp_sr TO sample_rate"); + + $this->_addSql("ALTER TABLE cc_files RENAME COLUMN temp_sr TO sample_rate"); $this->_addSql("ALTER TABLE cc_files RENAME COLUMN temp_br TO bit_rate"); - //add utime, lptime - $this->_addSql("ALTER TABLE cc_files ADD utime timestamp"); + //add utime, lptime + $this->_addSql("ALTER TABLE cc_files ADD utime timestamp"); $this->_addSql("ALTER TABLE cc_files ADD lptime timestamp"); - //setting these to a default now for timeline refresh purposes. - $now = gmdate("Y-m-d H:i:s"); + //setting these to a default now for timeline refresh purposes. + $now = gmdate("Y-m-d H:i:s"); $this->_addSql("UPDATE cc_files SET utime = '$now'"); } diff --git a/install_minimal/upgrades/airtime-2.1.0/common/Version20120410143340.php b/install_minimal/upgrades/airtime-2.1.0/common/Version20120410143340.php index a4468606b..9cd72c7fc 100644 --- a/install_minimal/upgrades/airtime-2.1.0/common/Version20120410143340.php +++ b/install_minimal/upgrades/airtime-2.1.0/common/Version20120410143340.php @@ -12,7 +12,7 @@ class Version20120410143340 extends AbstractMigration */ public function up(Schema $schema) { - //convert column creator to be creator_id on cc_playlist + //convert column creator to be creator_id on cc_playlist $this->_addSql("ALTER TABLE cc_playlist ADD creator_id integer"); $this->_addSql("UPDATE cc_playlist SET creator_id = (SELECT id FROM cc_subjs WHERE creator = login)"); $this->_addSql("ALTER TABLE cc_playlist DROP COLUMN creator"); diff --git a/install_minimal/upgrades/airtime-2.1.0/propel/airtime/CcShowInstances.php b/install_minimal/upgrades/airtime-2.1.0/propel/airtime/CcShowInstances.php index 9cf5407ca..260fee1f9 100644 --- a/install_minimal/upgrades/airtime-2.1.0/propel/airtime/CcShowInstances.php +++ b/install_minimal/upgrades/airtime-2.1.0/propel/airtime/CcShowInstances.php @@ -136,36 +136,36 @@ class CcShowInstances extends BaseCcShowInstances { ->update(array('DbPlayoutStatus' => 0), $con); } - /** - * Computes the value of the aggregate column time_filled - * - * @param PropelPDO $con A connection object - * - * @return mixed The scalar result from the aggregate query - */ - public function computeDbTimeFilled(PropelPDO $con) - { - $stmt = $con->prepare('SELECT SUM(clip_length) FROM "cc_schedule" WHERE cc_schedule.INSTANCE_ID = :p1'); - $stmt->bindValue(':p1', $this->getDbId()); - $stmt->execute(); - return $stmt->fetchColumn(); - } - - /** - * Updates the aggregate column time_filled - * - * @param PropelPDO $con A connection object - */ - public function updateDbTimeFilled(PropelPDO $con) - { - $this->setDbTimeFilled($this->computeDbTimeFilled($con)); - $this->save($con); + /** + * Computes the value of the aggregate column time_filled + * + * @param PropelPDO $con A connection object + * + * @return mixed The scalar result from the aggregate query + */ + public function computeDbTimeFilled(PropelPDO $con) + { + $stmt = $con->prepare('SELECT SUM(clip_length) FROM "cc_schedule" WHERE cc_schedule.INSTANCE_ID = :p1'); + $stmt->bindValue(':p1', $this->getDbId()); + $stmt->execute(); + return $stmt->fetchColumn(); + } + + /** + * Updates the aggregate column time_filled + * + * @param PropelPDO $con A connection object + */ + public function updateDbTimeFilled(PropelPDO $con) + { + $this->setDbTimeFilled($this->computeDbTimeFilled($con)); + $this->save($con); } public function preInsert(PropelPDO $con = null) { - $now = new DateTime("now", new DateTimeZone("UTC")); - $this->setDbCreated($now); - return true; + $now = new DateTime("now", new DateTimeZone("UTC")); + $this->setDbCreated($now); + return true; } } // CcShowInstances diff --git a/install_minimal/upgrades/upgrade-template/UpgradeCommon.php b/install_minimal/upgrades/upgrade-template/UpgradeCommon.php index 218ecfa3c..8792c89cf 100644 --- a/install_minimal/upgrades/upgrade-template/UpgradeCommon.php +++ b/install_minimal/upgrades/upgrade-template/UpgradeCommon.php @@ -26,31 +26,31 @@ class UpgradeCommon { */ public static function connectToDatabase($p_exitOnError = true) { - try { - $con = Propel::getConnection(); - } catch (Exception $e) { - echo $e->getMessage().PHP_EOL; - echo "Database connection problem.".PHP_EOL; - echo "Check if database exists with corresponding permissions.".PHP_EOL; - if ($p_exitOnError) { - exit(1); - } - return false; - } - return true; + try { + $con = Propel::getConnection(); + } catch (Exception $e) { + echo $e->getMessage().PHP_EOL; + echo "Database connection problem.".PHP_EOL; + echo "Check if database exists with corresponding permissions.".PHP_EOL; + if ($p_exitOnError) { + exit(1); + } + return false; + } + return true; } public static function DbTableExists($p_name) { - $con = Propel::getConnection(); - try { - $sql = "SELECT * FROM ".$p_name." LIMIT 1"; - $con->query($sql); - } catch (PDOException $e){ - return false; - } - return true; + $con = Propel::getConnection(); + try { + $sql = "SELECT * FROM ".$p_name." LIMIT 1"; + $con->query($sql); + } catch (PDOException $e){ + return false; + } + return true; } private static function GetAirtimeSrcDir() diff --git a/python_apps/api_clients/api_client.py b/python_apps/api_clients/api_client.py index 130724f66..5ca176ec2 100644 --- a/python_apps/api_clients/api_client.py +++ b/python_apps/api_clients/api_client.py @@ -20,6 +20,11 @@ import traceback AIRTIME_VERSION = "2.2.0" + +# TODO : Place these functions in some common module. Right now, media +# monitor uses the same functions and it would be better to reuse them +# instead of copy pasting them around + def to_unicode(obj, encoding='utf-8'): if isinstance(obj, basestring): if not isinstance(obj, unicode): @@ -39,7 +44,7 @@ def convert_dict_value_to_utf8(md): # Airtime API Client ################################################################################ -class AirtimeApiClient(): +class AirtimeApiClient(object): # This is a little hacky fix so that I don't have to pass the config object # everywhere where AirtimeApiClient needs to be initialized @@ -422,53 +427,46 @@ class AirtimeApiClient(): def send_media_monitor_requests(self, action_list, dry=False): """ - Send a gang of media monitor events at a time. actions_list is a list - of dictionaries where every dictionary is representing an action. Every - action dict must contain a 'mode' key that says what kind of action it - is and an optional 'is_record' key that says whether the show was - recorded or not. The value of this key does not matter, only if it's - present or not. + Send a gang of media monitor events at a time. actions_list is a + list of dictionaries where every dictionary is representing an + action. Every action dict must contain a 'mode' key that says + what kind of action it is and an optional 'is_record' key that + says whether the show was recorded or not. The value of this key + does not matter, only if it's present or not. """ - logger = self.logger - try: - url = self.construct_url('reload_metadata_group') - # We are assuming that action_list is a list of dictionaries such - # that every dictionary represents the metadata of a file along - # with a special mode key that is the action to be executed by the - # controller. - valid_actions = [] - # We could get a list of valid_actions in a much shorter way using - # filter but here we prefer a little more verbosity to help - # debugging - for action in action_list: - if not 'mode' in action: - self.logger.debug("Warning: Trying to send a request element without a 'mode'") - self.logger.debug("Here is the the request: '%s'" % str(action) ) - else: - # We alias the value of is_record to true or false no - # matter what it is based on if it's absent in the action - if 'is_record' not in action: - action['is_record'] = 0 - valid_actions.append(action) - # Note that we must prefix every key with: mdX where x is a number - # Is there a way to format the next line a little better? The - # parenthesis make the code almost unreadable - md_list = dict((("md%d" % i), json.dumps(convert_dict_value_to_utf8(md))) \ - for i,md in enumerate(valid_actions)) - # For testing we add the following "dry" parameter to tell the - # controller not to actually do any changes - if dry: md_list['dry'] = 1 - self.logger.info("Pumping out %d requests..." % len(valid_actions)) - data = urllib.urlencode(md_list) - req = urllib2.Request(url, data) - response = self.get_response_from_server(req) - response = json.loads(response) - return response - except ValueError: raise - except Exception, e: - logger.error('Exception: %s', e) - logger.error("traceback: %s", traceback.format_exc()) - raise + url = self.construct_url('reload_metadata_group') + # We are assuming that action_list is a list of dictionaries such + # that every dictionary represents the metadata of a file along + # with a special mode key that is the action to be executed by the + # controller. + valid_actions = [] + # We could get a list of valid_actions in a much shorter way using + # filter but here we prefer a little more verbosity to help + # debugging + for action in action_list: + if not 'mode' in action: + self.logger.debug("Warning: Trying to send a request element without a 'mode'") + self.logger.debug("Here is the the request: '%s'" % str(action) ) + else: + # We alias the value of is_record to true or false no + # matter what it is based on if it's absent in the action + if 'is_record' not in action: + action['is_record'] = 0 + valid_actions.append(action) + # Note that we must prefix every key with: mdX where x is a number + # Is there a way to format the next line a little better? The + # parenthesis make the code almost unreadable + md_list = dict((("md%d" % i), json.dumps(convert_dict_value_to_utf8(md))) \ + for i,md in enumerate(valid_actions)) + # For testing we add the following "dry" parameter to tell the + # controller not to actually do any changes + if dry: md_list['dry'] = 1 + self.logger.info("Pumping out %d requests..." % len(valid_actions)) + data = urllib.urlencode(md_list) + req = urllib2.Request(url, data) + response = self.get_response_from_server(req) + response = json.loads(response) + return response #returns a list of all db files for a given directory in JSON format: #{"files":["path/to/file1", "path/to/file2"]} diff --git a/python_apps/icecast2/airtime-icecast-status.xsl b/python_apps/icecast2/airtime-icecast-status.xsl index 565c81ebe..c69ba714d 100644 --- a/python_apps/icecast2/airtime-icecast-status.xsl +++ b/python_apps/icecast2/airtime-icecast-status.xsl @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/python_apps/media-monitor/airtimefilemonitor/airtimemetadata.py b/python_apps/media-monitor/airtimefilemonitor/airtimemetadata.py index f2963bea9..731f619f2 100644 --- a/python_apps/media-monitor/airtimefilemonitor/airtimemetadata.py +++ b/python_apps/media-monitor/airtimefilemonitor/airtimemetadata.py @@ -12,14 +12,14 @@ import traceback """ list of supported easy tags in mutagen version 1.20 -['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', -'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby', -'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid', -'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort', -'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor', -'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid', -'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid', -'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid', +['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', +'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby', +'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid', +'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort', +'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor', +'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid', +'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid', +'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid', 'arranger', 'albumsort', 'replaygain_*_peak', 'organization'] """ @@ -139,7 +139,7 @@ class AirtimeMetadata: def get_md_from_file(self, filepath): """ Returns None if error retrieving metadata. Otherwise returns a dictionary - representing the file's metadata + representing the file's metadata """ self.logger.info("getting info from filepath %s", filepath) @@ -248,9 +248,9 @@ class AirtimeMetadata: try: md['MDATA_KEY_BITRATE'] = getattr(file_info.info, "bitrate", 0) md['MDATA_KEY_SAMPLERATE'] = getattr(file_info.info, "sample_rate", 0) - + md['MDATA_KEY_DURATION'] = self.format_length(getattr(file_info.info, "length", 0.0)) - + md['MDATA_KEY_MIME'] = "" if len(file_info.mime) > 0: md['MDATA_KEY_MIME'] = file_info.mime[0] diff --git a/python_apps/media-monitor2/media/metadata/definitions.py b/python_apps/media-monitor2/media/metadata/definitions.py index 51ef6fd14..ec3bed48f 100644 --- a/python_apps/media-monitor2/media/metadata/definitions.py +++ b/python_apps/media-monitor2/media/metadata/definitions.py @@ -1,111 +1,143 @@ # -*- coding: utf-8 -*- -import media.monitor.process as md +import media.metadata.process as md +import re from os.path import normpath -from media.monitor.pure import format_length, file_md5 +from media.monitor.pure import format_length, file_md5, is_airtime_recorded, \ + no_extension_basename -with md.metadata('MDATA_KEY_DURATION') as t: - t.default(u'0.0') - t.depends('length') - t.translate(lambda k: format_length(k['length'])) +defs_loaded = False -with md.metadata('MDATA_KEY_MIME') as t: - t.default(u'') - t.depends('mime') - t.translate(lambda k: k['mime'].replace('-','/')) +def is_defs_loaded(): + global defs_loaded + return defs_loaded -with md.metadata('MDATA_KEY_BITRATE') as t: - t.default(u'') - t.depends('bitrate') - t.translate(lambda k: k['bitrate']) +def load_definitions(): + with md.metadata('MDATA_KEY_DURATION') as t: + t.default(u'0.0') + t.depends('length') + t.translate(lambda k: format_length(k['length'])) -with md.metadata('MDATA_KEY_SAMPLERATE') as t: - t.default(u'0') - t.depends('sample_rate') - t.translate(lambda k: k['sample_rate']) + with md.metadata('MDATA_KEY_MIME') as t: + t.default(u'') + t.depends('mime') + # Is this necessary? + t.translate(lambda k: k['mime'].replace('audio/vorbis','audio/ogg')) -with md.metadata('MDATA_KEY_FTYPE'): - t.depends('ftype') # i don't think this field even exists - t.default(u'audioclip') - t.translate(lambda k: k['ftype']) # but just in case + with md.metadata('MDATA_KEY_BITRATE') as t: + t.default(u'') + t.depends('bitrate') + t.translate(lambda k: k['bitrate']) -with md.metadata("MDATA_KEY_CREATOR") as t: - t.depends("artist") - # A little kludge to make sure that we have some value for when we parse - # MDATA_KEY_TITLE - t.default(u"") - t.max_length(512) + with md.metadata('MDATA_KEY_SAMPLERATE') as t: + t.default(u'0') + t.depends('sample_rate') + t.translate(lambda k: k['sample_rate']) -with md.metadata("MDATA_KEY_SOURCE") as t: - t.depends("album") - t.max_length(512) + with md.metadata('MDATA_KEY_FTYPE') as t: + t.depends('ftype') # i don't think this field even exists + t.default(u'audioclip') + t.translate(lambda k: k['ftype']) # but just in case -with md.metadata("MDATA_KEY_GENRE") as t: - t.depends("genre") - t.max_length(64) + with md.metadata("MDATA_KEY_CREATOR") as t: + t.depends("artist") + # A little kludge to make sure that we have some value for when we parse + # MDATA_KEY_TITLE + t.default(u"") + t.max_length(512) -with md.metadata("MDATA_KEY_MOOD") as t: - t.depends("mood") - t.max_length(64) + with md.metadata("MDATA_KEY_SOURCE") as t: + t.depends("album") + t.max_length(512) -with md.metadata("MDATA_KEY_TRACKNUMBER") as t: - t.depends("tracknumber") + with md.metadata("MDATA_KEY_GENRE") as t: + t.depends("genre") + t.max_length(64) -with md.metadata("MDATA_KEY_BPM") as t: - t.depends("bpm") - t.max_length(8) + with md.metadata("MDATA_KEY_MOOD") as t: + t.depends("mood") + t.max_length(64) -with md.metadata("MDATA_KEY_LABEL") as t: - t.depends("organization") - t.max_length(512) + with md.metadata("MDATA_KEY_TRACKNUMBER") as t: + t.depends("tracknumber") -with md.metadata("MDATA_KEY_COMPOSER") as t: - t.depends("composer") - t.max_length(512) + with md.metadata("MDATA_KEY_BPM") as t: + t.depends("bpm") + t.max_length(8) -with md.metadata("MDATA_KEY_ENCODER") as t: - t.depends("encodedby") - t.max_length(512) + with md.metadata("MDATA_KEY_LABEL") as t: + t.depends("organization") + t.max_length(512) -with md.metadata("MDATA_KEY_CONDUCTOR") as t: - t.depends("conductor") - t.max_length(512) + with md.metadata("MDATA_KEY_COMPOSER") as t: + t.depends("composer") + t.max_length(512) -with md.metadata("MDATA_KEY_YEAR") as t: - t.depends("date") - t.max_length(16) + with md.metadata("MDATA_KEY_ENCODER") as t: + t.depends("encodedby") + t.max_length(512) -with md.metadata("MDATA_KEY_URL") as t: - t.depends("website") + with md.metadata("MDATA_KEY_CONDUCTOR") as t: + t.depends("conductor") + t.max_length(512) -with md.metadata("MDATA_KEY_ISRC") as t: - t.depends("isrc") - t.max_length(512) + with md.metadata("MDATA_KEY_YEAR") as t: + t.depends("date") + t.max_length(16) -with md.metadata("MDATA_KEY_COPYRIGHT") as t: - t.depends("copyright") - t.max_length(512) + with md.metadata("MDATA_KEY_URL") as t: + t.depends("website") -with md.metadata("MDATA_KEY_FILEPATH") as t: - t.depends('path') - t.translate(lambda k: normpath(k['path'])) + with md.metadata("MDATA_KEY_ISRC") as t: + t.depends("isrc") + t.max_length(512) -with md.metadata("MDATA_KEY_MD5") as t: - t.depends('path') - t.optional(False) - t.translate(lambda k: file_md5(k['path'], max_length=100)) + with md.metadata("MDATA_KEY_COPYRIGHT") as t: + t.depends("copyright") + t.max_length(512) -# owner is handled differently by (by events.py) + with md.metadata("MDATA_KEY_ORIGINAL_PATH") as t: + t.depends('path') + t.translate(lambda k: unicode(normpath(k['path']))) -with md.metadata('MDATA_KEY_ORIGINAL_PATH') as t: - t.depends('original_path') + with md.metadata("MDATA_KEY_MD5") as t: + t.depends('path') + t.optional(False) + t.translate(lambda k: file_md5(k['path'], max_length=100)) -# MDATA_KEY_TITLE is the annoying special case -with md.metadata('MDATA_KEY_TITLE') as t: - # Need to know MDATA_KEY_CREATOR to know if show was recorded. Value is - # defaulted to "" from definitions above - t.depends('title','MDATA_KEY_CREATOR') - t.max_length(512) + # owner is handled differently by (by events.py) -with md.metadata('MDATA_KEY_LABEL') as t: - t.depends('label') - t.max_length(512) + # MDATA_KEY_TITLE is the annoying special case b/c we sometimes read it + # from file name + + + # must handle 3 cases: + # 1. regular case (not recorded + title is present) + # 2. title is absent (read from file) + # 3. recorded file + def tr_title(k): + #unicode_unknown = u"unknown" + new_title = u"" + if is_airtime_recorded(k) or k['title'] != u"": + new_title = k['title'] + else: + default_title = no_extension_basename(k['path']) + default_title = re.sub(r'__\d+\.',u'.', default_title) + + # format is: track_number-title-123kbps.mp3 + m = re.match(".+?-(?P.+)-(\d+kbps|unknown)$", default_title) + if m: new_title = m.group('title') + else: new_title = re.sub(r'-\d+kbps$', u'', default_title) + + return new_title + + with md.metadata('MDATA_KEY_TITLE') as t: + # Need to know MDATA_KEY_CREATOR to know if show was recorded. Value is + # defaulted to "" from definitions above + t.depends('title','MDATA_KEY_CREATOR','path') + t.optional(False) + t.translate(tr_title) + t.max_length(512) + + with md.metadata('MDATA_KEY_LABEL') as t: + t.depends('label') + t.max_length(512) diff --git a/python_apps/media-monitor2/media/metadata/process.py b/python_apps/media-monitor2/media/metadata/process.py index a4c361468..b500d029d 100644 --- a/python_apps/media-monitor2/media/metadata/process.py +++ b/python_apps/media-monitor2/media/metadata/process.py @@ -1,14 +1,36 @@ # -*- coding: utf-8 -*- from contextlib import contextmanager from media.monitor.pure import truncate_to_length, toposort +from os.path import normpath +from media.monitor.exceptions import BadSongFile +from media.monitor.log import Loggable +import media.monitor.pure as mmp +from collections import namedtuple import mutagen +class FakeMutagen(dict): + """ + Need this fake mutagen object so that airtime_special functions + return a proper default value instead of throwing an exceptions for + files that mutagen doesn't recognize + """ + FakeInfo = namedtuple('FakeInfo','length bitrate') + def __init__(self,path): + self.path = path + self.mime = ['audio/wav'] + self.info = FakeMutagen.FakeInfo(0.0, '') + dict.__init__(self) + def set_length(self,l): + old_bitrate = self.info.bitrate + self.info = FakeMutagen.FakeInfo(l, old_bitrate) + class MetadataAbsent(Exception): def __init__(self, name): self.name = name def __str__(self): return "Could not obtain element '%s'" % self.name -class MetadataElement(object): +class MetadataElement(Loggable): + def __init__(self,name): self.name = name # "Sane" defaults @@ -18,6 +40,7 @@ class MetadataElement(object): self.__default = None self.__is_normalized = lambda _ : True self.__max_length = -1 + self.__translator = None def max_length(self,l): self.__max_length = l @@ -57,31 +80,64 @@ class MetadataElement(object): return self.__path def __slice_deps(self, d): + """ + returns a dictionary of all the key value pairs in d that are also + present in self.__deps + """ return dict( (k,v) for k,v in d.iteritems() if k in self.__deps) def __str__(self): return "%s(%s)" % (self.name, ' '.join(list(self.__deps))) def read_value(self, path, original, running={}): - # If value is present and normalized then we don't touch it + + # If value is present and normalized then we only check if it's + # normalized or not. We normalize if it's not normalized already + + if self.name in original: v = original[self.name] if self.__is_normalized(v): return v else: return self.__normalizer(v) - # A dictionary slice with all the dependencies and their values + # We slice out only the dependencies that are required for the metadata + # element. dep_slice_orig = self.__slice_deps(original) dep_slice_running = self.__slice_deps(running) + # TODO : remove this later + dep_slice_special = self.__slice_deps({'path' : path}) + # We combine all required dependencies into a single dictionary + # that we will pass to the translator full_deps = dict( dep_slice_orig.items() - + dep_slice_running.items() ) + + dep_slice_running.items() + + dep_slice_special.items()) # check if any dependencies are absent - if len(full_deps) != len(self.__deps) or len(self.__deps) == 0: + # note: there is no point checking the case that len(full_deps) > + # len(self.__deps) because we make sure to "slice out" any supefluous + # dependencies above. + if len(full_deps) != len(self.dependencies()) or \ + len(self.dependencies()) == 0: # If we have a default value then use that. Otherwise throw an # exception if self.has_default(): return self.get_default() else: raise MetadataAbsent(self.name) + # We have all dependencies. Now for actual for parsing + def def_translate(dep): + def wrap(k): + e = [ x for x in dep ][0] + return k[e] + return wrap + + # Only case where we can select a default translator + if self.__translator is None: + self.translate(def_translate(self.dependencies())) + if len(self.dependencies()) > 2: # dependencies include themselves + self.logger.info("Ignoring some dependencies in translate %s" + % self.name) + self.logger.info(self.dependencies()) + r = self.__normalizer( self.__translator(full_deps) ) if self.__max_length != -1: r = truncate_to_length(r, self.__max_length) @@ -92,24 +148,40 @@ def normalize_mutagen(path): Consumes a path and reads the metadata using mutagen. normalizes some of the metadata that isn't read through the mutagen hash """ - m = mutagen.File(path, easy=True) + if not mmp.file_playable(path): raise BadSongFile(path) + try : m = mutagen.File(path, easy=True) + except Exception : raise BadSongFile(path) + if m is None: m = FakeMutagen(path) + try: + if mmp.extension(path) == 'wav': + m.set_length(mmp.read_wave_duration(path)) + except Exception: raise BadSongFile(path) md = {} for k,v in m.iteritems(): - if type(v) is list: md[k] = v[0] + if type(v) is list: + if len(v) > 0: md[k] = v[0] else: md[k] = v # populate special metadata values - md['length'] = getattr(m.info, u'length', 0.0) + md['length'] = getattr(m.info, 'length', 0.0) md['bitrate'] = getattr(m.info, 'bitrate', u'') md['sample_rate'] = getattr(m.info, 'sample_rate', 0) md['mime'] = m.mime[0] if len(m.mime) > 0 else u'' - md['path'] = path + md['path'] = normpath(path) + if 'title' not in md: md['title'] = u'' return md + +class OverwriteMetadataElement(Exception): + def __init__(self, m): self.m = m + def __str__(self): return "Trying to overwrite: %s" % self.m + class MetadataReader(object): def __init__(self): self.clear() def register_metadata(self,m): + if m in self.__mdata_name_map: + raise OverwriteMetadataElement(m) self.__mdata_name_map[m.name] = m d = dict( (name,m.dependencies()) for name,m in self.__mdata_name_map.iteritems() ) @@ -131,6 +203,9 @@ class MetadataReader(object): if not mdata.is_optional(): raise return normalized_metadata + def read_mutagen(self, path): + return self.read(path, normalize_mutagen(path)) + global_reader = MetadataReader() @contextmanager diff --git a/python_apps/media-monitor2/media/monitor/events.py b/python_apps/media-monitor2/media/monitor/events.py index 0b2a92e14..761ffddb7 100644 --- a/python_apps/media-monitor2/media/monitor/events.py +++ b/python_apps/media-monitor2/media/monitor/events.py @@ -148,6 +148,9 @@ class BaseEvent(Loggable): owners.remove_file_owner(self.path) return ret except BadSongFile as e: return [e] + except Exception as e: + self.unexpected_exception(e) + return[e] # nothing to see here, please move along def morph_into(self, evt): diff --git a/python_apps/media-monitor2/media/monitor/listeners.py b/python_apps/media-monitor2/media/monitor/listeners.py index 72b8ca8aa..b33a5c1a9 100644 --- a/python_apps/media-monitor2/media/monitor/listeners.py +++ b/python_apps/media-monitor2/media/monitor/listeners.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import pyinotify from pydispatch import dispatcher +from functools import wraps import media.monitor.pure as mmp from media.monitor.pure import IncludeOnly @@ -31,6 +32,7 @@ class FileMediator(object): def unignore(path): FileMediator.ignored_set.remove(path) def mediate_ignored(fn): + @wraps(fn) def wrapped(self, event, *args,**kwargs): event.pathname = unicode(event.pathname, "utf-8") if FileMediator.is_ignored(event.pathname): @@ -49,15 +51,11 @@ class OrganizeListener(BaseListener, pyinotify.ProcessEvent, Loggable): def process_IN_CLOSE_WRITE(self, event): #self.logger.info("===> handling: '%s'" % str(event)) self.process_to_organize(event) - # got cookie + def process_IN_MOVED_TO(self, event): #self.logger.info("===> handling: '%s'" % str(event)) self.process_to_organize(event) - def process_default(self, event): - pass - #self.logger.info("===> Not handling: '%s'" % str(event)) - def flush_events(self, path): """ organize the whole directory at path. (pretty much by doing what @@ -67,8 +65,9 @@ class OrganizeListener(BaseListener, pyinotify.ProcessEvent, Loggable): for f in mmp.walk_supported(path, clean_empties=True): self.logger.info("Bootstrapping: File in 'organize' directory: \ '%s'" % f) - dispatcher.send(signal=self.signal, sender=self, - event=OrganizeFile(f)) + if not mmp.file_locked(f): + dispatcher.send(signal=self.signal, sender=self, + event=OrganizeFile(f)) flushed += 1 #self.logger.info("Flushed organized directory with %d files" % flushed) diff --git a/python_apps/media-monitor2/media/monitor/log.py b/python_apps/media-monitor2/media/monitor/log.py index 5d632bbf4..7e75c719d 100644 --- a/python_apps/media-monitor2/media/monitor/log.py +++ b/python_apps/media-monitor2/media/monitor/log.py @@ -6,37 +6,30 @@ from media.monitor.pure import LazyProperty appname = 'root' def setup_logging(log_path): - """ - Setup logging by writing log to 'log_path' - """ + """ Setup logging by writing log to 'log_path' """ #logger = logging.getLogger(appname) logging.basicConfig(filename=log_path, level=logging.DEBUG) def get_logger(): - """ - in case we want to use the common logger from a procedural interface - """ + """ in case we want to use the common logger from a procedural + interface """ return logging.getLogger() class Loggable(object): - """ - Any class that wants to log can inherit from this class and automatically - get a logger attribute that can be used like: self.logger.info(...) etc. - """ + """ Any class that wants to log can inherit from this class and + automatically get a logger attribute that can be used like: + self.logger.info(...) etc. """ __metaclass__ = abc.ABCMeta @LazyProperty def logger(self): return get_logger() def unexpected_exception(self,e): - """ - Default message for 'unexpected' exceptions - """ + """ Default message for 'unexpected' exceptions """ self.fatal_exception("'Unexpected' exception has occured:", e) def fatal_exception(self, message, e): - """ - Prints an exception 'e' with 'message'. Also outputs the traceback. - """ + """ Prints an exception 'e' with 'message'. Also outputs the + traceback. """ self.logger.error( message ) self.logger.error( str(e) ) self.logger.error( traceback.format_exc() ) diff --git a/python_apps/media-monitor2/media/monitor/manager.py b/python_apps/media-monitor2/media/monitor/manager.py index 77bcf10ec..a2fc47028 100644 --- a/python_apps/media-monitor2/media/monitor/manager.py +++ b/python_apps/media-monitor2/media/monitor/manager.py @@ -18,7 +18,7 @@ class ManagerTimeout(threading.Thread,Loggable): secnods. This used to be just a work around for cc-4235 but recently became a permanent solution because it's "cheap" and reliable """ - def __init__(self, manager, interval=3): + def __init__(self, manager, interval=1.5): # TODO : interval should be read from config and passed here instead # of just using the hard coded value threading.Thread.__init__(self) @@ -26,7 +26,7 @@ class ManagerTimeout(threading.Thread,Loggable): self.interval = interval def run(self): while True: - time.sleep(self.interval) # every 3 seconds + time.sleep(self.interval) self.manager.flush_organize() class Manager(Loggable): @@ -178,7 +178,7 @@ class Manager(Loggable): # the OrganizeListener instance will walk path and dispatch an organize # event for every file in that directory self.organize['organize_listener'].flush_events(new_path) - self.__add_watch(new_path, self.organize['organize_listener']) + #self.__add_watch(new_path, self.organize['organize_listener']) def flush_organize(self): path = self.organize['organize_path'] @@ -202,6 +202,12 @@ class Manager(Loggable): organize. """ store_paths = mmp.expand_storage(store) + # First attempt to make sure that all paths exist before adding any + # watches + for path_type, path in store_paths.iteritems(): + try: mmp.create_dir(path) + except mmp.FailedToCreateDir as e: self.unexpected_exception(e) + self.set_problem_files_path(store_paths['problem_files']) self.set_imported_path(store_paths['imported']) self.set_recorded_path(store_paths['recorded']) diff --git a/python_apps/media-monitor2/media/monitor/metadata.py b/python_apps/media-monitor2/media/monitor/metadata.py index bd42e434c..d5dba3b51 100644 --- a/python_apps/media-monitor2/media/monitor/metadata.py +++ b/python_apps/media-monitor2/media/monitor/metadata.py @@ -2,15 +2,19 @@ import mutagen import os import copy -from collections import namedtuple from mutagen.easymp4 import EasyMP4KeyError from mutagen.easyid3 import EasyID3KeyError from media.monitor.exceptions import BadSongFile, InvalidMetadataElement from media.monitor.log import Loggable -from media.monitor.pure import format_length, truncate_to_length +from media.monitor.pure import format_length import media.monitor.pure as mmp +# emf related stuff +from media.metadata.process import global_reader +import media.metadata.definitions as defs +defs.load_definitions() + """ list of supported easy tags in mutagen version 1.20 ['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', @@ -43,21 +47,6 @@ airtime2mutagen = { "MDATA_KEY_COPYRIGHT" : "copyright", } -class FakeMutagen(dict): - """ - Need this fake mutagen object so that airtime_special functions - return a proper default value instead of throwing an exceptions for - files that mutagen doesn't recognize - """ - FakeInfo = namedtuple('FakeInfo','length bitrate') - def __init__(self,path): - self.path = path - self.mime = ['audio/wav'] - self.info = FakeMutagen.FakeInfo(0.0, '') - dict.__init__(self) - def set_length(self,l): - old_bitrate = self.info.bitrate - self.info = FakeMutagen.FakeInfo(l, old_bitrate) # Some airtime attributes are special because they must use the mutagen object # itself to calculate the value that they need. The lambda associated with each @@ -100,6 +89,7 @@ class Metadata(Loggable): # little bit messy. Some of the handling is in m.m.pure while the rest is # here. Also interface is not very consistent + # TODO : what is this shit? maybe get rid of it? @staticmethod def fix_title(path): # If we have no title in path we will format it @@ -110,39 +100,6 @@ class Metadata(Loggable): m[u'title'] = new_title m.save() - @staticmethod - def airtime_dict(d): - """ - Converts mutagen dictionary 'd' into airtime dictionary - """ - temp_dict = {} - for m_key, m_val in d.iteritems(): - # TODO : some files have multiple fields for the same metadata. - # genre is one example. In that case mutagen will return a list - # of values - - if isinstance(m_val, list): - # TODO : does it make more sense to just skip the element in - # this case? - if len(m_val) == 0: assign_val = '' - else: assign_val = m_val[0] - else: assign_val = m_val - - temp_dict[ m_key ] = assign_val - airtime_dictionary = {} - for muta_k, muta_v in temp_dict.iteritems(): - # We must check if we can actually translate the mutagen key into - # an airtime key before doing the conversion - if muta_k in mutagen2airtime: - airtime_key = mutagen2airtime[muta_k] - # Apply truncation in the case where airtime_key is in our - # truncation table - muta_v = \ - truncate_to_length(muta_v, truncate_table[airtime_key])\ - if airtime_key in truncate_table else muta_v - airtime_dictionary[ airtime_key ] = muta_v - return airtime_dictionary - @staticmethod def write_unsafe(path,md): """ @@ -157,6 +114,7 @@ class Metadata(Loggable): if airtime_k in airtime2mutagen: # The unicode cast here is mostly for integers that need to be # strings + if airtime_v is None: continue try: song_file[ airtime2mutagen[airtime_k] ] = unicode(airtime_v) except (EasyMP4KeyError, EasyID3KeyError) as e: @@ -170,44 +128,7 @@ class Metadata(Loggable): # Forcing the unicode through try : fpath = fpath.decode("utf-8") except : pass - - if not mmp.file_playable(fpath): raise BadSongFile(fpath) - - try : full_mutagen = mutagen.File(fpath, easy=True) - except Exception : raise BadSongFile(fpath) - - self.path = fpath - if not os.path.exists(self.path): - self.logger.info("Attempting to read metadata of file \ - that does not exist. Setting metadata to {}") - self.__metadata = {} - return - # TODO : Simplify the way all of these rules are handled right now it's - # extremely unclear and needs to be refactored. - #if full_mutagen is None: raise BadSongFile(fpath) - if full_mutagen is None: full_mutagen = FakeMutagen(fpath) - self.__metadata = Metadata.airtime_dict(full_mutagen) - # Now we extra the special values that are calculated from the mutagen - # object itself: - - if mmp.extension(fpath) == 'wav': - full_mutagen.set_length(mmp.read_wave_duration(fpath)) - - for special_key,f in airtime_special.iteritems(): - try: - new_val = f(full_mutagen) - if new_val is not None: - self.__metadata[special_key] = new_val - except Exception as e: - self.logger.info("Could not get special key %s for %s" % - (special_key, fpath)) - self.logger.info(str(e)) - # Finally, we "normalize" all the metadata here: - self.__metadata = mmp.normalized_metadata(self.__metadata, fpath) - # Now we must load the md5: - # TODO : perhaps we shouldn't hard code how many bytes we're reading - # from the file? - self.__metadata['MDATA_KEY_MD5'] = mmp.file_md5(fpath,max_length=100) + self.__metadata = global_reader.read_mutagen(fpath) def is_recorded(self): """ diff --git a/python_apps/media-monitor2/media/monitor/organizer.py b/python_apps/media-monitor2/media/monitor/organizer.py index bb3c0cb02..ce1849b90 100644 --- a/python_apps/media-monitor2/media/monitor/organizer.py +++ b/python_apps/media-monitor2/media/monitor/organizer.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import time import media.monitor.pure as mmp import media.monitor.owners as owners from media.monitor.handler import ReportHandler @@ -11,14 +10,12 @@ from os.path import dirname import os.path class Organizer(ReportHandler,Loggable): - """ - Organizer is responsible to to listening to OrganizeListener events - and committing the appropriate changes to the filesystem. It does - not in any interact with WatchSyncer's even when the the WatchSyncer - is a "storage directory". The "storage" directory picks up all of - its events through pyinotify. (These events are fed to it through - StoreWatchListener) - """ + """ Organizer is responsible to to listening to OrganizeListener + events and committing the appropriate changes to the filesystem. + It does not in any interact with WatchSyncer's even when the the + WatchSyncer is a "storage directory". The "storage" directory picks + up all of its events through pyinotify. (These events are fed to it + through StoreWatchListener) """ # Commented out making this class a singleton because it's just a band aid # for the real issue. The real issue being making multiple Organizer @@ -42,11 +39,9 @@ class Organizer(ReportHandler,Loggable): super(Organizer, self).__init__(signal=self.channel, weak=False) def handle(self, sender, event): - """ - Intercept events where a new file has been added to the organize - directory and place it in the correct path (starting with - self.target_path) - """ + """ Intercept events where a new file has been added to the + organize directory and place it in the correct path (starting + with self.target_path) """ # Only handle this event type assert isinstance(event, OrganizeFile), \ "Organizer can only handle OrganizeFile events.Given '%s'" % event @@ -72,13 +67,9 @@ class Organizer(ReportHandler,Loggable): directory=d) return cb - time.sleep(0.05) - mmp.magic_move(event.path, new_path, after_dir_make=new_dir_watch(dirname(new_path))) - time.sleep(0.05) - # The reason we need to go around saving the owner in this ass # backwards way is bewcause we are unable to encode the owner id # into the file itself so that the StoreWatchListener listener can diff --git a/python_apps/media-monitor2/media/monitor/pure.py b/python_apps/media-monitor2/media/monitor/pure.py index ec8f1695a..a879f449f 100644 --- a/python_apps/media-monitor2/media/monitor/pure.py +++ b/python_apps/media-monitor2/media/monitor/pure.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import copy +from subprocess import Popen, PIPE import subprocess import os import math @@ -21,7 +22,6 @@ from configobj import ConfigObj from media.monitor.exceptions import FailedToSetLocale, FailedToCreateDir -#supported_extensions = [u"mp3", u"ogg", u"oga"] supported_extensions = [u"mp3", u"ogg", u"oga", u"flac", u"wav", u'm4a', u'mp4'] @@ -66,7 +66,6 @@ class IncludeOnly(object): return func(moi, event, *args, **kwargs) return _wrap - def partition(f, alist): """ Partition is very similar to filter except that it also returns the @@ -92,14 +91,13 @@ def is_file_supported(path): # TODO : In the future we would like a better way to find out whether a show # has been recorded def is_airtime_recorded(md): - """ - Takes a metadata dictionary and returns True if it belongs to a file that - was recorded by Airtime. - """ + """ Takes a metadata dictionary and returns True if it belongs to a + file that was recorded by Airtime. """ if not 'MDATA_KEY_CREATOR' in md: return False return md['MDATA_KEY_CREATOR'] == u'Airtime Show Recorder' def read_wave_duration(path): + """ Read the length of .wav file (mutagen does not handle this) """ with contextlib.closing(wave.open(path,'r')) as f: frames = f.getnframes() rate = f.getframerate() @@ -107,9 +105,7 @@ def read_wave_duration(path): return duration def clean_empty_dirs(path): - """ - walks path and deletes every empty directory it finds - """ + """ walks path and deletes every empty directory it finds """ # TODO : test this function if path.endswith('/'): clean_empty_dirs(path[0:-1]) else: @@ -154,22 +150,25 @@ def no_extension_basename(path): else: return '.'.join(base.split(".")[0:-1]) def walk_supported(directory, clean_empties=False): - """ - A small generator wrapper around os.walk to only give us files that support - the extensions we are considering. When clean_empties is True we - recursively delete empty directories left over in directory after the walk. - """ + """ A small generator wrapper around os.walk to only give us files + that support the extensions we are considering. When clean_empties + is True we recursively delete empty directories left over in + directory after the walk. """ for root, dirs, files in os.walk(directory): full_paths = ( os.path.join(root, name) for name in files if is_file_supported(name) ) for fp in full_paths: yield fp if clean_empties: clean_empty_dirs(directory) + +def file_locked(path): + cmd = "lsof %s" % path + f = Popen(cmd, shell=True, stdout=PIPE).stdout + return bool(f.readlines()) + def magic_move(old, new, after_dir_make=lambda : None): - """ - Moves path old to new and constructs the necessary to directories for new - along the way - """ + """ Moves path old to new and constructs the necessary to + directories for new along the way """ new_dir = os.path.dirname(new) if not os.path.exists(new_dir): os.makedirs(new_dir) # We need this crusty hack because anytime a directory is created we must @@ -179,18 +178,15 @@ def magic_move(old, new, after_dir_make=lambda : None): shutil.move(old,new) def move_to_dir(dir_path,file_path): - """ - moves a file at file_path into dir_path/basename(filename) - """ + """ moves a file at file_path into dir_path/basename(filename) """ bs = os.path.basename(file_path) magic_move(file_path, os.path.join(dir_path, bs)) def apply_rules_dict(d, rules): - """ - Consumes a dictionary of rules that maps some keys to lambdas which it - applies to every matching element in d and returns a new dictionary with - the rules applied. If a rule returns none then it's not applied - """ + """ Consumes a dictionary of rules that maps some keys to lambdas + which it applies to every matching element in d and returns a new + dictionary with the rules applied. If a rule returns none then it's + not applied """ new_d = copy.deepcopy(d) for k, rule in rules.iteritems(): if k in d: @@ -205,17 +201,14 @@ def default_to_f(dictionary, keys, default, condition): return new_d def default_to(dictionary, keys, default): - """ - Checks if the list of keys 'keys' exists in 'dictionary'. If not then it - returns a new dictionary with all those missing keys defaults to 'default' - """ + """ Checks if the list of keys 'keys' exists in 'dictionary'. If + not then it returns a new dictionary with all those missing keys + defaults to 'default' """ cnd = lambda dictionary, key: key not in dictionary return default_to_f(dictionary, keys, default, cnd) def remove_whitespace(dictionary): - """ - Remove values that empty whitespace in the dictionary - """ + """ Remove values that empty whitespace in the dictionary """ nd = copy.deepcopy(dictionary) bad_keys = [] for k,v in nd.iteritems(): @@ -227,6 +220,7 @@ def remove_whitespace(dictionary): return nd def parse_int(s): + # TODO : this function isn't used anywhere yet but it may useful for emf """ Tries very hard to get some sort of integer result from s. Defaults to 0 when it fails @@ -242,53 +236,6 @@ def parse_int(s): try : return str(reduce(op.add, takewhile(lambda x: x.isdigit(), s))) except: return None -def normalized_metadata(md, original_path): - """ - consumes a dictionary of metadata and returns a new dictionary with the - formatted meta data. We also consume original_path because we must set - MDATA_KEY_CREATOR based on in it sometimes - """ - new_md = copy.deepcopy(md) - # replace all slashes with dashes - #for k,v in new_md.iteritems(): new_md[k] = unicode(v).replace('/','-') - # Specific rules that are applied in a per attribute basis - format_rules = { - 'MDATA_KEY_TRACKNUMBER' : parse_int, - 'MDATA_KEY_FILEPATH' : lambda x: os.path.normpath(x), - 'MDATA_KEY_BPM' : lambda x: x[0:8], - 'MDATA_KEY_MIME' : lambda x: x.replace('audio/vorbis','audio/ogg'), - # Whenever 0 is reported we change it to empty - #'MDATA_KEY_BITRATE' : lambda x: '' if str(x) == '0' else x - } - - new_md = remove_whitespace(new_md) # remove whitespace fields - # Format all the fields in format_rules - new_md = apply_rules_dict(new_md, format_rules) - # set filetype to audioclip by default - new_md = default_to(dictionary=new_md, keys=['MDATA_KEY_FTYPE'], - default=u'audioclip') - - # Try to parse bpm but delete the whole key if that fails - if 'MDATA_KEY_BPM' in new_md: - new_md['MDATA_KEY_BPM'] = parse_int(new_md['MDATA_KEY_BPM']) - if new_md['MDATA_KEY_BPM'] is None: - del new_md['MDATA_KEY_BPM'] - - if not is_airtime_recorded(new_md): - # Read title from filename if it does not exist - default_title = no_extension_basename(original_path) - default_title = re.sub(r'__\d+\.',u'.', default_title) - if re.match(".+-%s-.+$" % unicode_unknown, default_title): - default_title = u'' - new_md = default_to(dictionary=new_md, keys=['MDATA_KEY_TITLE'], - default=default_title) - new_md['MDATA_KEY_TITLE'] = re.sub(r'-\d+kbps$', u'', - new_md['MDATA_KEY_TITLE']) - - # TODO : wtf is this for again? - new_md['MDATA_KEY_TITLE'] = re.sub(r'-?%s-?' % unicode_unknown, u'', - new_md['MDATA_KEY_TITLE']) - return new_md def organized_path(old_path, root_path, orig_md): """ @@ -348,10 +295,9 @@ def organized_path(old_path, root_path, orig_md): # TODO : Get rid of this function and every one of its uses. We no longer use # the md5 signature of a song for anything def file_md5(path,max_length=100): - """ - Get md5 of file path (if it exists). Use only max_length characters to save - time and memory. Pass max_length=-1 to read the whole file (like in mm1) - """ + """ Get md5 of file path (if it exists). Use only max_length + characters to save time and memory. Pass max_length=-1 to read the + whole file (like in mm1) """ if os.path.exists(path): with open(path, 'rb') as f: m = hashlib.md5() @@ -367,16 +313,12 @@ def encode_to(obj, encoding='utf-8'): return obj def convert_dict_value_to_utf8(md): - """ - formats a dictionary to send as a request to api client - """ + """ formats a dictionary to send as a request to api client """ return dict([(item[0], encode_to(item[1], "utf-8")) for item in md.items()]) def get_system_locale(locale_path='/etc/default/locale'): - """ - Returns the configuration object for the system's default locale. Normally - requires root access. - """ + """ Returns the configuration object for the system's default + locale. Normally requires root access. """ if os.path.exists(locale_path): try: config = ConfigObj(locale_path) @@ -386,9 +328,7 @@ def get_system_locale(locale_path='/etc/default/locale'): permissions issue?" % locale_path) def configure_locale(config): - """ - sets the locale according to the system's locale. - """ + """ sets the locale according to the system's locale. """ current_locale = locale.getlocale() if current_locale[1] is None: default_locale = locale.getdefaultlocale() @@ -405,27 +345,21 @@ def configure_locale(config): def fondle(path,times=None): # TODO : write unit tests for this - """ - touch a file to change the last modified date. Beware of calling this - function on the same file from multiple threads. - """ + """ touch a file to change the last modified date. Beware of calling + this function on the same file from multiple threads. """ with file(path, 'a'): os.utime(path, times) def last_modified(path): - """ - return the time of the last time mm2 was ran. path refers to the index file - whose date modified attribute contains this information. In the case when - the file does not exist we set this time 0 so that any files on the - filesystem were modified after it - """ + """ return the time of the last time mm2 was ran. path refers to the + index file whose date modified attribute contains this information. + In the case when the file does not exist we set this time 0 so that + any files on the filesystem were modified after it """ if os.path.exists(path): return os.path.getmtime(path) else: return 0 def expand_storage(store): - """ - A storage directory usually consists of 4 different subdirectories. This - function returns their paths - """ + """ A storage directory usually consists of 4 different + subdirectories. This function returns their paths """ store = os.path.normpath(store) return { 'organize' : os.path.join(store, 'organize'), @@ -435,10 +369,8 @@ def expand_storage(store): } def create_dir(path): - """ - will try and make sure that path exists at all costs. raises an exception - if it fails at this task. - """ + """ will try and make sure that path exists at all costs. raises an + exception if it fails at this task. """ if not os.path.exists(path): try : os.makedirs(path) except Exception as e : raise FailedToCreateDir(path, e) @@ -456,11 +388,10 @@ def sub_path(directory,f): return common == normalized def owner_id(original_path): - """ - Given 'original_path' return the file name of the of 'identifier' file. - return the id that is contained in it. If no file is found or nothing is - read then -1 is returned. File is deleted after the number has been read - """ + """ Given 'original_path' return the file name of the of + 'identifier' file. return the id that is contained in it. If no file + is found or nothing is read then -1 is returned. File is deleted + after the number has been read """ fname = "%s.identifier" % original_path owner_id = -1 try: @@ -476,9 +407,8 @@ def owner_id(original_path): return owner_id def file_playable(pathname): - """ - Returns True if 'pathname' is playable by liquidsoap. False otherwise. - """ + """ Returns True if 'pathname' is playable by liquidsoap. False + otherwise. """ # when there is an single apostrophe inside of a string quoted by # apostrophes, we can only escape it by replace that apostrophe with # '\''. This breaks the string into two, and inserts an escaped @@ -514,18 +444,14 @@ def toposort(data): assert not data, "A cyclic dependency exists amongst %r" % data def truncate_to_length(item, length): - """ - Truncates 'item' to 'length' - """ + """ Truncates 'item' to 'length' """ if isinstance(item, int): item = str(item) if isinstance(item, basestring): if len(item) > length: return item[0:length] else: return item def format_length(mutagen_length): - """ - Convert mutagen length to airtime length - """ + """ Convert mutagen length to airtime length """ t = float(mutagen_length) h = int(math.floor(t / 3600)) t = t % 3600 diff --git a/python_apps/media-monitor2/media/monitor/request.py b/python_apps/media-monitor2/media/monitor/request.py new file mode 100644 index 000000000..a2f3cdc8f --- /dev/null +++ b/python_apps/media-monitor2/media/monitor/request.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +import threading + +from media.monitor.exceptions import BadSongFile +from media.monitor.log import Loggable +import api_clients.api_client as ac + +class ThreadedRequestSync(threading.Thread, Loggable): + def __init__(self, rs): + threading.Thread.__init__(self) + self.rs = rs + self.daemon = True + self.start() + + def run(self): + self.rs.run_request() + +class RequestSync(Loggable): + """ This class is responsible for making the api call to send a + request to airtime. In the process it packs the requests and retries + for some number of times """ + @classmethod + def create_with_api_client(cls, watcher, requests): + apiclient = ac.AirtimeApiClient.create_right_config() + self = cls(watcher, requests, apiclient) + return self + + def __init__(self, watcher, requests, apiclient): + self.watcher = watcher + self.requests = requests + self.apiclient = apiclient + + def run_request(self): + self.logger.info("Attempting request with %d items." % + len(self.requests)) + packed_requests = [] + for request_event in self.requests: + try: + for request in request_event.safe_pack(): + if isinstance(request, BadSongFile): + self.logger.info("Bad song file: '%s'" % request.path) + else: packed_requests.append(request) + except Exception as e: + self.unexpected_exception( e ) + if hasattr(request_event, 'path'): + self.logger.info("Possibly related to path: '%s'" % + request_event.path) + try: self.apiclient.send_media_monitor_requests( packed_requests ) + # most likely we did not get json response as we expected + except ValueError: + self.logger.info("ApiController.php probably crashed, we \ + diagnose this from the fact that it did not return \ + valid json") + self.logger.info("Trying again after %f seconds" % + self.request_wait) + except Exception as e: self.unexpected_exception(e) + else: self.logger.info("Request was successful") + self.watcher.flag_done() # poor man's condition variable + diff --git a/python_apps/media-monitor2/media/monitor/syncdb.py b/python_apps/media-monitor2/media/monitor/syncdb.py index 0c14fb038..cc8abc294 100644 --- a/python_apps/media-monitor2/media/monitor/syncdb.py +++ b/python_apps/media-monitor2/media/monitor/syncdb.py @@ -53,11 +53,11 @@ class AirtimeDB(Loggable): """ return self.id_to_dir[ dir_id ] - def storage_path(self): return self.base_storage - def organize_path(self): return self.storage_paths['organize'] - def problem_path(self): return self.storage_paths['problem_files'] - def import_path(self): return self.storage_paths['imported'] - def recorded_path(self): return self.storage_paths['recorded'] + def storage_path(self) : return self.base_storage + def organize_path(self) : return self.storage_paths['organize'] + def problem_path(self) : return self.storage_paths['problem_files'] + def import_path(self) : return self.storage_paths['imported'] + def recorded_path(self) : return self.storage_paths['recorded'] def list_watched(self): """ diff --git a/python_apps/media-monitor2/media/monitor/watchersyncer.py b/python_apps/media-monitor2/media/monitor/watchersyncer.py index e7a2cf9c0..d2df9ed3a 100644 --- a/python_apps/media-monitor2/media/monitor/watchersyncer.py +++ b/python_apps/media-monitor2/media/monitor/watchersyncer.py @@ -6,69 +6,9 @@ import copy from media.monitor.handler import ReportHandler from media.monitor.log import Loggable from media.monitor.exceptions import BadSongFile -from media.monitor.pure import LazyProperty from media.monitor.eventcontractor import EventContractor from media.monitor.events import EventProxy - -import api_clients.api_client as ac - -class RequestSync(threading.Thread,Loggable): - """ - This class is responsible for making the api call to send a request - to airtime. In the process it packs the requests and retries for - some number of times - """ - def __init__(self, watcher, requests): - threading.Thread.__init__(self) - self.watcher = watcher - self.requests = requests - self.retries = 1 - self.request_wait = 0.3 - - @LazyProperty - def apiclient(self): - return ac.AirtimeApiClient.create_right_config() - - def run(self): - self.logger.info("Attempting request with %d items." % - len(self.requests)) - # Note that we must attach the appropriate mode to every - # response. Also Not forget to attach the 'is_record' to any - # requests that are related to recorded shows - # TODO : recorded shows aren't flagged right - # Is this retry shit even necessary? Consider getting rid of this. - packed_requests = [] - for request_event in self.requests: - try: - for request in request_event.safe_pack(): - if isinstance(request, BadSongFile): - self.logger.info("Bad song file: '%s'" % request.path) - else: packed_requests.append(request) - except Exception as e: - self.unexpected_exception( e ) - if hasattr(request_event, 'path'): - self.logger.info("Possibly related to path: '%s'" % - request_event.path) - def make_req(): - self.apiclient.send_media_monitor_requests( packed_requests ) - for try_index in range(0,self.retries): - try: make_req() - # most likely we did not get json response as we expected - except ValueError: - self.logger.info("ApiController.php probably crashed, we \ - diagnose this from the fact that it did not return \ - valid json") - self.logger.info("Trying again after %f seconds" % - self.request_wait) - time.sleep( self.request_wait ) - except Exception as e: self.unexpected_exception(e) - else: - self.logger.info("Request worked on the '%d' try" % - (try_index + 1)) - break - else: self.logger.info("Failed to send request after '%d' tries..." % - self.retries) - self.watcher.flag_done() +from media.monitor.request import ThreadedRequestSync, RequestSync class TimeoutWatcher(threading.Thread,Loggable): """ @@ -131,8 +71,7 @@ class WatchSyncer(ReportHandler,Loggable): #self.push_queue( event ) except BadSongFile as e: self.fatal_exception("Received bas song file '%s'" % e.path, e) - except Exception as e: - self.unexpected_exception(e) + except Exception as e: self.unexpected_exception(e) else: self.logger.info("Received event that does not implement packing.\ Printing its representation:") @@ -209,8 +148,8 @@ class WatchSyncer(ReportHandler,Loggable): requests = copy.copy(self.__queue) def launch_request(): # Need shallow copy here - t = RequestSync(watcher=self, requests=requests) - t.start() + t = ThreadedRequestSync( RequestSync.create_with_api_client( + watcher=self, requests=requests) ) self.__current_thread = t self.__requests.append(launch_request) self.__reset_queue() @@ -218,7 +157,8 @@ class WatchSyncer(ReportHandler,Loggable): def __reset_queue(self): self.__queue = [] def __del__(self): - # Ideally we would like to do a little more to ensure safe shutdown + #this destructor is completely untested and it's unclear whether + #it's even doing anything useful. consider removing it if self.events_in_queue(): self.logger.warn("Terminating with events still in the queue...") if self.requests_in_queue(): diff --git a/python_apps/media-monitor2/media/update/replaygain.py b/python_apps/media-monitor2/media/update/replaygain.py index a630a2655..5af7cd4a1 100644 --- a/python_apps/media-monitor2/media/update/replaygain.py +++ b/python_apps/media-monitor2/media/update/replaygain.py @@ -71,8 +71,10 @@ def get_file_type(file_path): def calculate_replay_gain(file_path): """ - This function accepts files of type mp3/ogg/flac and returns a calculated ReplayGain value in dB. - If the value cannot be calculated for some reason, then we default to 0 (Unity Gain). + This function accepts files of type mp3/ogg/flac and returns a calculated + ReplayGain value in dB. + If the value cannot be calculated for some reason, then we default to 0 + (Unity Gain). http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_1.0_specification """ @@ -92,20 +94,37 @@ def calculate_replay_gain(file_path): if file_type: if file_type == 'mp3': if run_process("which mp3gain > /dev/null") == 0: - out = get_process_output('nice -n %s mp3gain -q "%s" 2> /dev/null' % (nice_level, temp_file_path)) - search = re.search(r'Recommended "Track" dB change: (.*)', out) + command = 'nice -n %s mp3gain -q "%s" 2> /dev/null' \ + % (nice_level, temp_file_path) + out = get_process_output(command) + search = re.search(r'Recommended "Track" dB change: (.*)', \ + out) else: logger.warn("mp3gain not found") elif file_type == 'vorbis': - if run_process("which vorbisgain > /dev/null && which ogginfo > /dev/null") == 0: - run_process('nice -n %s vorbisgain -q -f "%s" 2>/dev/null >/dev/null' % (nice_level,temp_file_path)) + command = "which vorbisgain > /dev/null && which ogginfo > \ + /dev/null" + if run_process(command) == 0: + command = 'nice -n %s vorbisgain -q -f "%s" 2>/dev/null \ + >/dev/null' % (nice_level,temp_file_path) + run_process(command) + out = get_process_output('ogginfo "%s"' % temp_file_path) search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out) else: logger.warn("vorbisgain/ogginfo not found") elif file_type == 'flac': if run_process("which metaflac > /dev/null") == 0: - out = get_process_output('nice -n %s metaflac --show-tag=REPLAYGAIN_TRACK_GAIN "%s"' % (nice_level, temp_file_path)) + + command = 'nice -n %s metaflac --add-replay-gain "%s"' \ + % (nice_level, temp_file_path) + run_process(command) + + command = 'nice -n %s metaflac \ + --show-tag=REPLAYGAIN_TRACK_GAIN "%s"' \ + % (nice_level, temp_file_path) + + out = get_process_output(command) search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out) else: logger.warn("metaflac not found") diff --git a/python_apps/media-monitor2/mm2.py b/python_apps/media-monitor2/mm2.py index 17731cad8..217cfcfc8 100644 --- a/python_apps/media-monitor2/mm2.py +++ b/python_apps/media-monitor2/mm2.py @@ -59,7 +59,8 @@ def main(global_config, api_client_config, log_config, try: with open(config['index_path'], 'w') as f: f.write(" ") except Exception as e: - log.info("Failed to create index file with exception: %s" % str(e)) + log.info("Failed to create index file with exception: %s" \ + % str(e)) else: log.info("Created index file, reloading configuration:") main( global_config, api_client_config, log_config, @@ -103,6 +104,9 @@ def main(global_config, api_client_config, log_config, airtime_notifier = AirtimeNotifier(config, airtime_receiver) store = apiclient.setup_media_monitor() + + log.info("Initing with the following airtime response:%s" % str(store)) + airtime_receiver.change_storage({ 'directory':store[u'stor'] }) for watch_dir in store[u'watched_dirs']: @@ -114,6 +118,7 @@ def main(global_config, api_client_config, log_config, (given from the database)." % watch_dir) if os.path.exists(watch_dir): airtime_receiver.new_watch({ 'directory':watch_dir }, restart=True) + else: log.info("Failed to add watch on %s" % str(watch_dir)) bs = Bootstrapper( db=sdb, watch_signal='watch' ) diff --git a/python_apps/media-monitor2/tests/test_api_client.py b/python_apps/media-monitor2/tests/test_api_client.py index 26f1a25ef..18595f860 100644 --- a/python_apps/media-monitor2/tests/test_api_client.py +++ b/python_apps/media-monitor2/tests/test_api_client.py @@ -19,8 +19,8 @@ class TestApiClient(unittest.TestCase): self.apc.register_component("api-client-tester") # All of the following requests should error out in some way self.bad_requests = [ - { 'mode' : 'dang it', 'is_record' : 0 }, - { 'mode' : 'damn frank', 'is_record' : 1 }, + { 'mode' : 'foo', 'is_record' : 0 }, + { 'mode' : 'bar', 'is_record' : 1 }, { 'no_mode' : 'at_all' }, ] def test_bad_requests(self): diff --git a/python_apps/media-monitor2/tests/test_emf.py b/python_apps/media-monitor2/tests/test_emf.py new file mode 100644 index 000000000..140c0aa8b --- /dev/null +++ b/python_apps/media-monitor2/tests/test_emf.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import unittest +#from pprint import pprint as pp + +from media.metadata.process import global_reader +from media.monitor.metadata import Metadata + +import media.metadata.definitions as defs +defs.load_definitions() + +class TestMMP(unittest.TestCase): + + def setUp(self): + self.maxDiff = None + + def metadatas(self,f): + return global_reader.read_mutagen(f), Metadata(f).extract() + + def test_old_metadata(self): + path = "/home/rudi/music/Nightingale.mp3" + m = global_reader.read_mutagen(path) + self.assertTrue( len(m) > 0 ) + n = Metadata(path) + self.assertEqual(n.extract(), m) + + def test_recorded(self): + recorded_file = "./15:15:00-Untitled Show-256kbps.ogg" + emf, old = self.metadatas(recorded_file) + self.assertEqual(emf, old) + +if __name__ == '__main__': unittest.main() diff --git a/python_apps/media-monitor2/tests/test_metadata.py b/python_apps/media-monitor2/tests/test_metadata.py index 6f24240b0..7a32b61a8 100644 --- a/python_apps/media-monitor2/tests/test_metadata.py +++ b/python_apps/media-monitor2/tests/test_metadata.py @@ -26,7 +26,6 @@ class TestMetadata(unittest.TestCase): i += 1 print("Sample metadata: '%s'" % md) self.assertTrue( len( md.keys() ) > 0 ) - self.assertTrue( 'MDATA_KEY_MD5' in md ) utf8 = md_full.utf8() for k,v in md.iteritems(): if hasattr(utf8[k], 'decode'): @@ -42,10 +41,4 @@ class TestMetadata(unittest.TestCase): x1 = 123456 print("Formatting '%s' to '%s'" % (x1, mmm.format_length(x1))) - def test_truncate_to_length(self): - s1 = "testing with non string literal" - s2 = u"testing with unicode literal" - self.assertEqual( len(mmm.truncate_to_length(s1, 5)), 5) - self.assertEqual( len(mmm.truncate_to_length(s2, 8)), 8) - if __name__ == '__main__': unittest.main() diff --git a/python_apps/media-monitor2/tests/test_metadata_def.py b/python_apps/media-monitor2/tests/test_metadata_def.py new file mode 100644 index 000000000..e666ef68a --- /dev/null +++ b/python_apps/media-monitor2/tests/test_metadata_def.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import unittest + +import media.metadata.process as md + +class TestMetadataDef(unittest.TestCase): + def test_simple(self): + + with md.metadata('MDATA_TESTING') as t: + t.optional(True) + t.depends('ONE','TWO') + t.default('unknown') + t.translate(lambda kw: kw['ONE'] + kw['TWO']) + + h = { 'ONE' : "testing", 'TWO' : "123" } + result = md.global_reader.read('test_path',h) + self.assertTrue( 'MDATA_TESTING' in result ) + self.assertEqual( result['MDATA_TESTING'], 'testing123' ) + h1 = { 'ONE' : 'big testing', 'two' : 'nothing' } + result1 = md.global_reader.read('bs path', h1) + self.assertEqual( result1['MDATA_TESTING'], 'unknown' ) + + def test_topo(self): + with md.metadata('MDATA_TESTING') as t: + t.depends('shen','sheni') + t.default('megitzda') + t.translate(lambda kw: kw['shen'] + kw['sheni']) + + with md.metadata('shen') as t: + t.default('vaxo') + + with md.metadata('sheni') as t: + t.default('gio') + + with md.metadata('vaxo') as t: + t.depends('shevetsi') + + v = md.global_reader.read('bs mang', {}) + self.assertEqual(v['MDATA_TESTING'], 'vaxogio') + self.assertTrue( 'vaxo' not in v ) + + md.global_reader.clear() + +if __name__ == '__main__': unittest.main() diff --git a/python_apps/media-monitor2/tests/test_pure.py b/python_apps/media-monitor2/tests/test_pure.py index 64d09dc62..6bc5d4906 100644 --- a/python_apps/media-monitor2/tests/test_pure.py +++ b/python_apps/media-monitor2/tests/test_pure.py @@ -2,7 +2,6 @@ import unittest import os import media.monitor.pure as mmp -from media.monitor.metadata import Metadata class TestMMP(unittest.TestCase): def setUp(self): @@ -34,68 +33,6 @@ class TestMMP(unittest.TestCase): sd = mmp.default_to(dictionary=sd, keys=def_keys, default='DEF') for k in def_keys: self.assertEqual( sd[k], 'DEF' ) - def test_normalized_metadata(self): - #Recorded show test first - orig = Metadata.airtime_dict({ - 'date' : [u'2012-08-21'], - 'tracknumber' : [u'2'], - 'title' : [u'record-2012-08-21-11:29:00'], - 'artist' : [u'Airtime Show Recorder'] - }) - orga = Metadata.airtime_dict({ - 'date' : [u'2012-08-21'], - 'tracknumber' : [u'2'], - 'artist' : [u'Airtime Show Recorder'], - 'title' : [u'record-2012-08-21-11:29:00'] - }) - orga['MDATA_KEY_FTYPE'] = u'audioclip' - orig['MDATA_KEY_BITRATE'] = u'256000' - orga['MDATA_KEY_BITRATE'] = u'256000' - old_path = "/home/rudi/recorded/2012-08-21-11:29:00.ogg" - normalized = mmp.normalized_metadata(orig, old_path) - normalized['MDATA_KEY_BITRATE'] = u'256000' - - self.assertEqual( orga, normalized ) - - organized_base_name = "11:29:00-record-256kbps.ogg" - base = "/srv/airtime/stor/" - organized_path = mmp.organized_path(old_path,base, normalized) - self.assertEqual(os.path.basename(organized_path), organized_base_name) - - def test_normalized_metadata2(self): - """ - cc-4305 - """ - orig = Metadata.airtime_dict({ - 'date' : [u'2012-08-27'], - 'tracknumber' : [u'3'], - 'title' : [u'18-11-00-Untitled Show'], - 'artist' : [u'Airtime Show Recorder'] - }) - old_path = "/home/rudi/recorded/doesnt_really_matter.ogg" - normalized = mmp.normalized_metadata(orig, old_path) - normalized['MDATA_KEY_BITRATE'] = u'256000' - opath = mmp.organized_path(old_path, "/srv/airtime/stor/", - normalized) - # TODO : add a better test than this... - self.assertTrue( len(opath) > 0 ) - - def test_normalized_metadata3(self): - """ - Test the case where the metadata is empty - """ - orig = Metadata.airtime_dict({}) - paths_unknown_title = [ - ("/testin/unknown-unknown-unknown.mp3",""), - ("/testin/01-unknown-123kbps.mp3",""), - ("/testin/02-unknown-140kbps.mp3",""), - ("/testin/unknown-unknown-123kbps.mp3",""), - ("/testin/unknown-bibimbop-unknown.mp3","bibimbop"), - ] - for p,res in paths_unknown_title: - normalized = mmp.normalized_metadata(orig, p) - self.assertEqual( normalized['MDATA_KEY_TITLE'], res) - def test_file_md5(self): p = os.path.realpath(__file__) m1 = mmp.file_md5(p) @@ -116,6 +53,13 @@ class TestMMP(unittest.TestCase): self.assertEqual( mmp.parse_int("123asf"), "123" ) self.assertEqual( mmp.parse_int("asdf"), None ) + def test_truncate_to_length(self): + s1 = "testing with non string literal" + s2 = u"testing with unicode literal" + self.assertEqual( len(mmp.truncate_to_length(s1, 5)), 5) + self.assertEqual( len(mmp.truncate_to_length(s2, 8)), 8) + + def test_owner_id(self): start_path = "testing.mp3" id_path = "testing.mp3.identifier" diff --git a/python_apps/media-monitor2/tests/test_requestsync.py b/python_apps/media-monitor2/tests/test_requestsync.py new file mode 100644 index 000000000..2570bc34e --- /dev/null +++ b/python_apps/media-monitor2/tests/test_requestsync.py @@ -0,0 +1,48 @@ +import unittest +from mock import MagicMock + +from media.monitor.request import RequestSync + +class TestRequestSync(unittest.TestCase): + + def apc_mock(self): + fake_apc = MagicMock() + fake_apc.send_media_monitor_requests = MagicMock() + return fake_apc + + def watcher_mock(self): + fake_watcher = MagicMock() + fake_watcher.flag_done = MagicMock() + return fake_watcher + + def request_mock(self): + fake_request = MagicMock() + fake_request.safe_pack = MagicMock(return_value=[]) + return fake_request + + def test_send_media_monitor(self): + fake_apc = self.apc_mock() + fake_requests = [ self.request_mock() for x in range(1,5) ] + fake_watcher = self.watcher_mock() + rs = RequestSync(fake_watcher, fake_requests, fake_apc) + rs.run_request() + self.assertEquals(fake_apc.send_media_monitor_requests.call_count, 1) + + def test_flag_done(self): + fake_apc = self.apc_mock() + fake_requests = [ self.request_mock() for x in range(1,5) ] + fake_watcher = self.watcher_mock() + rs = RequestSync(fake_watcher, fake_requests, fake_apc) + rs.run_request() + self.assertEquals(fake_watcher.flag_done.call_count, 1) + + def test_safe_pack(self): + fake_apc = self.apc_mock() + fake_requests = [ self.request_mock() for x in range(1,5) ] + fake_watcher = self.watcher_mock() + rs = RequestSync(fake_watcher, fake_requests, fake_apc) + rs.run_request() + for req in fake_requests: + self.assertEquals(req.safe_pack.call_count, 1) + +if __name__ == '__main__': unittest.main() diff --git a/python_apps/pypo/liquidsoap_scripts/library/external-todo.liq b/python_apps/pypo/liquidsoap_scripts/library/external-todo.liq deleted file mode 100644 index 24ae18cdc..000000000 --- a/python_apps/pypo/liquidsoap_scripts/library/external-todo.liq +++ /dev/null @@ -1,314 +0,0 @@ -# These operators need to be updated.. - - -# Stream data from mplayer -# @category Source / Input -# @param s data URI. -# @param ~restart restart on exit. -# @param ~restart_on_error restart on exit with error. -# @param ~buffer Duration of the pre-buffered data. -# @param ~max Maximum duration of the buffered data. -def input.mplayer(~id="input.mplayer", - ~restart=true,~restart_on_error=false, - ~buffer=0.2,~max=10.,s) = - input.external(id=id,restart=restart, - restart_on_error=restart_on_error, - buffer=buffer,max=max, - "mplayer -really-quiet -ao pcm:file=/dev/stdout \ - -vc null -vo null #{quote(s)} 2>/dev/null") -end - - -# Output the stream using aplay. -# Using this turns "root.sync" to false -# since aplay will do the synchronisation -# @category Source / Output -# @param ~id Output's ID -# @param ~device Alsa pcm device name -# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop. -# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped. -# @param ~on_start Callback executed when outputting starts. -# @param ~on_stop Callback executed when outputting stops. -# @param s Source to play -def output.aplay(~id="output.aplay",~device="default", - ~fallible=false,~on_start={()},~on_stop={()}, - ~restart_on_crash=false,s) - def aplay_p(m) = - "aplay -D #{device}" - end - log(label=id,level=3,"Setting root.sync to false") - set("root.sync",false) - output.pipe.external(id=id, - fallible=fallible,on_start=on_start,on_stop=on_stop, - restart_on_crash=restart_on_crash, - restart_on_new_track=false, - process=aplay_p,s) -end - -%ifdef output.icecast.external -# Output to icecast using the lame command line encoder. -# @category Source / Output -# @param ~id Output's ID -# @param ~start Start output threads on operator initialization. -# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed. -# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled. -# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop. -# @param ~restart_on_new_track Restart encoder upon new track. -# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds. -# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users. -# @param ~lame The lame binary -# @param ~bitrate Encoder bitrate -# @param ~swap Swap audio samples. Depends on local machine's endianess and lame's version. Test this parameter if you experience garbaged mp3 audio data. On intel 32 and 64 architectures, the parameter should be "true" for lame version >= 3.98. -# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty. -# @param ~protocol Protocol of the streaming server: 'http' for Icecast, 'icy' for Shoutcast. -# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped. -# @param ~on_start Callback executed when outputting starts. -# @param ~on_stop Callback executed when outputting stops. -# @param s The source to output -def output.icecast.lame( - ~id="output.icecast.lame",~start=true, - ~restart=false,~restart_delay=3, - ~host="localhost",~port=8000, - ~user="source",~password="hackme", - ~genre="Misc",~url="http://savonet.sf.net/", - ~description="Liquidsoap Radio!",~public=true, - ~dumpfile="",~mount="Use [name]", - ~name="Use [mount]",~protocol="http", - ~lame="lame",~bitrate=128,~swap=false, - ~fallible=false,~on_start={()},~on_stop={()}, - ~restart_on_crash=false,~restart_on_new_track=false, - ~restart_encoder_delay=3600,~headers=[],s) - samplerate = get(default=44100,"frame.samplerate") - samplerate = float_of_int(samplerate) / 1000. - channels = get(default=2,"frame.channels") - swap = if swap then "-x" else "" end - mode = - if channels == 2 then - "j" # Encoding in joint stereo.. - else - "m" - end - # Metadata update is set by ICY with icecast - def lame_p(m) - "#{lame} -b #{bitrate} -r --bitwidth 16 -s #{samplerate} \ - --signed -m #{mode} --nores #{swap} -t - -" - end - output.icecast.external(id=id, - process=lame_p,bitrate=bitrate,start=start, - restart=restart,restart_delay=restart_delay, - host=host,port=port,user=user,password=password, - genre=genre,url=url,description=description, - public=public,dumpfile=dumpfile,restart_encoder_delay=restart_encoder_delay, - name=name,mount=mount,protocol=protocol, - header=false,restart_on_crash=restart_on_crash, - restart_on_new_track=restart_on_new_track,headers=headers, - fallible=fallible,on_start=on_start,on_stop=on_stop, - s) -end - -# Output to shoutcast using the lame encoder. -# @category Source / Output -# @param ~id Output's ID -# @param ~start Start output threads on operator initialization. -# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed. -# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled. -# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop. -# @param ~restart_on_new_track Restart encoder upon new track. -# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds. -# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users. -# @param ~lame The lame binary -# @param ~bitrate Encoder bitrate -# @param ~icy_reset Reset shoutcast source buffer upon connecting (necessary for NSV). -# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty. -# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped. -# @param ~on_start Callback executed when outputting starts. -# @param ~on_stop Callback executed when outputting stops. -# @param s The source to output -def output.shoutcast.lame( - ~id="output.shoutcast.mp3",~start=true, - ~restart=false,~restart_delay=3, - ~host="localhost",~port=8000, - ~user="source",~password="hackme", - ~genre="Misc",~url="http://savonet.sf.net/", - ~description="Liquidsoap Radio!",~public=true, - ~dumpfile="",~name="Use [mount]",~icy_reset=true, - ~lame="lame",~aim="",~icq="",~irc="", - ~fallible=false,~on_start={()},~on_stop={()}, - ~restart_on_crash=false,~restart_on_new_track=false, - ~restart_encoder_delay=3600,~bitrate=128,s) = - icy_reset = if icy_reset then "1" else "0" end - headers = [("icy-aim",aim),("icy-irc",irc), - ("icy-icq",icq),("icy-reset",icy_reset)] - output.icecast.lame( - id=id, headers=headers, lame=lame, - bitrate=bitrate, start=start, - restart=restart, restart_encoder_delay=restart_encoder_delay, - host=host, port=port, user=user, password=password, - genre=genre, url=url, description=description, - public=public, dumpfile=dumpfile, - restart_on_crash=restart_on_crash, - restart_on_new_track=restart_on_new_track, - name=name, mount="/", protocol="icy", - fallible=fallible,on_start=on_start,on_stop=on_stop, - s) -end - -# Output to icecast using the flac command line encoder. -# @category Source / Output -# @param ~id Output's ID -# @param ~start Start output threads on operator initialization. -# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed. -# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled. -# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop. -# @param ~restart_on_new_track Restart encoder upon new track. If false, the resulting stream will have a single track. -# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds. -# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users. -# @param ~flac The flac binary -# @param ~quality Encoder quality (0..8) -# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty. -# @param ~protocol Protocol of the streaming server: 'http' for Icecast, 'icy' for Shoutcast. -# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped. -# @param ~on_start Callback executed when outputting starts. -# @param ~on_stop Callback executed when outputting stops. -# @param s The source to output -def output.icecast.flac( - ~id="output.icecast.flac",~start=true, - ~restart=false,~restart_delay=3, - ~host="localhost",~port=8000, - ~user="source",~password="hackme", - ~genre="Misc",~url="http://savonet.sf.net/", - ~description="Liquidsoap Radio!",~public=true, - ~dumpfile="",~mount="Use [name]", - ~name="Use [mount]",~protocol="http", - ~flac="flac",~quality=6, - ~restart_on_crash=false, - ~restart_on_new_track=true, - ~restart_encoder_delay=(-1), - ~fallible=false,~on_start={()},~on_stop={()}, - s) - # We will use raw format, to - # bypass input length value in WAV - # header (input length is not known) - channels = get(default=2,"frame.channels") - samplerate = get(default=44100,"frame.samplerate") - def flac_p(m)= - def option(x) = - "-T #{quote(fst(x))}=#{quote(snd(x))}" - end - m = list.map(option,m) - m = string.concat(separator=" ",m) - "#{flac} --force-raw-format --endian=little --channels=#{channels} \ - --bps=16 --sample-rate=#{samplerate} --sign=signed #{m} \ - -#{quality} --ogg -c -" - end - output.icecast.external(id=id, - process=flac_p,bitrate=(-1),start=start, - restart=restart,restart_delay=restart_delay, - host=host,port=port,user=user,password=password, - genre=genre,url=url,description=description, - public=public,dumpfile=dumpfile, - name=name,mount=mount,protocol=protocol, - fallible=fallible,on_start=on_start,on_stop=on_stop, - restart_on_new_track=restart_on_new_track, - format="ogg",header=false,icy_metadata=false, - restart_on_crash=restart_on_crash, - restart_encoder_delay=restart_encoder_delay, - s) -end - -# Output to icecast using the aacplusenc command line encoder. -# @category Source / Output -# @param ~id Output's ID -# @param ~start Start output threads on operator initialization. -# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed. -# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled. -# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop. -# @param ~restart_on_new_track Restart encoder upon new track. -# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds. -# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users. -# @param ~aacplusenc The aacplusenc binary -# @param ~bitrate Encoder bitrate -# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty. -# @param ~protocol Protocol of the streaming server: 'http' for Icecast, 'icy' for Shoutcast. -# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped. -# @param ~on_start Callback executed when outputting starts. -# @param ~on_stop Callback executed when outputting stops. -# @param s The source to output -def output.icecast.aacplusenc( - ~id="output.icecast.aacplusenc",~start=true, - ~restart=false,~restart_delay=3, - ~host="localhost",~port=8000, - ~user="source",~password="hackme", - ~genre="Misc",~url="http://savonet.sf.net/", - ~description="Liquidsoap Radio!",~public=true, - ~dumpfile="",~mount="Use [name]", - ~name="Use [mount]",~protocol="http", - ~aacplusenc="aacplusenc",~bitrate=64, - ~fallible=false,~on_start={()},~on_stop={()}, - ~restart_on_crash=false,~restart_on_new_track=false, - ~restart_encoder_delay=3600,~headers=[],s) - # Metadata update is set by ICY with icecast - def aacplusenc_p(m) - "#{aacplusenc} - - #{bitrate}" - end - output.icecast.external(id=id, - process=aacplusenc_p,bitrate=bitrate,start=start, - restart=restart,restart_delay=restart_delay, - host=host,port=port,user=user,password=password, - genre=genre,url=url,description=description, - public=public,dumpfile=dumpfile, - name=name,mount=mount,protocol=protocol, - fallible=fallible,on_start=on_start,on_stop=on_stop, - header=true,restart_on_crash=restart_on_crash, - restart_on_new_track=restart_on_new_track,headers=headers, - restart_encoder_delay=restart_encoder_delay,format="audio/aacp",s) -end - -# Output to shoutcast using the aacplusenc encoder. -# @category Source / Output -# @param ~id Output's ID -# @param ~start Start output threads on operator initialization. -# @param ~restart Restart output after a failure. By default, liquidsoap will stop if the output failed. -# @param ~restart_delay Delay, in seconds, before attempting new connection, if restart is enabled. -# @param ~restart_on_crash Restart external process on crash. If false, liquidsoap will stop. -# @param ~restart_on_new_track Restart encoder upon new track. -# @param ~restart_encoder_delay Restart the encoder after this delay, in seconds. -# @param ~user User for shout source connection. Useful only in special cases, like with per-mountpoint users. -# @param ~aacplusenc The aacplusenc binary -# @param ~bitrate Encoder bitrate -# @param ~icy_reset Reset shoutcast source buffer upon connecting (necessary for NSV). -# @param ~dumpfile Dump stream to file, for debugging purpose. Disabled if empty. -# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped. -# @param ~on_start Callback executed when outputting starts. -# @param ~on_stop Callback executed when outputting stops. -# @param s The source to output -def output.shoutcast.aacplusenc( - ~id="output.shoutcast.aacplusenc",~start=true, - ~restart=false,~restart_delay=3, - ~host="localhost",~port=8000, - ~user="source",~password="hackme", - ~genre="Misc",~url="http://savonet.sf.net/", - ~description="Liquidsoap Radio!",~public=true, - ~fallible=false,~on_start={()},~on_stop={()}, - ~dumpfile="",~name="Use [mount]",~icy_reset=true, - ~aim="",~icq="",~irc="",~aacplusenc="aacplusenc", - ~restart_on_crash=false,~restart_on_new_track=false, - ~restart_encoder_delay=3600,~bitrate=64,s) = - icy_reset = if icy_reset then "1" else "0" end - headers = [("icy-aim",aim),("icy-irc",irc), - ("icy-icq",icq),("icy-reset",icy_reset)] - output.icecast.aacplusenc( - id=id, headers=headers, aacplusenc=aacplusenc, - bitrate=bitrate, start=start, - restart=restart, restart_delay=restart_delay, - host=host, port=port, user=user, password=password, - genre=genre, url=url, description=description, - public=public, dumpfile=dumpfile, - fallible=fallible,on_start=on_start,on_stop=on_stop, - restart_on_crash=restart_on_crash, restart_encoder_delay=restart_encoder_delay, - restart_on_new_track=restart_on_new_track, - name=name, mount="/", protocol="icy", - s) -end -%endif - diff --git a/python_apps/pypo/liquidsoap_scripts/library/externals.liq b/python_apps/pypo/liquidsoap_scripts/library/externals.liq index 6e1b98a60..b56f13d0c 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/externals.liq +++ b/python_apps/pypo/liquidsoap_scripts/library/externals.liq @@ -4,6 +4,7 @@ # Enable external Musepack decoder. Requires the # mpcdec binary in the path. Does not work on # Win32. +# @category Liquidsoap def enable_external_mpc_decoder() = # A list of know extensions and content-type for Musepack. # Values from http://en.wikipedia.org/wiki/Musepack diff --git a/python_apps/pypo/liquidsoap_scripts/library/gstreamer.liq b/python_apps/pypo/liquidsoap_scripts/library/gstreamer.liq new file mode 100644 index 000000000..d3a364abd --- /dev/null +++ b/python_apps/pypo/liquidsoap_scripts/library/gstreamer.liq @@ -0,0 +1,34 @@ +%ifdef input.gstreamer.video +# Stream from a video4linux 2 input device, such as a webcam. +# @category Source / Input +# @param ~id Force the value of the source ID. +# @param ~clock_safe Force the use of the dedicated v4l clock. +# @param ~device V4L2 device to use. +def input.v4l2(~id="",~clock_safe=true,~device="/dev/video0") + pipeline = "v4l2src device=#{device}" + input.gstreamer.video(id=id, clock_safe=clock_safe, pipeline=pipeline) +end + +# Stream from a video4linux 2 input device, such as a webcam. +# @category Source / Input +# @param ~id Force the value of the source ID. +# @param ~clock_safe Force the use of the dedicated v4l clock. +# @param ~device V4L2 device to use. +def input.v4l2_with_audio(~id="",~clock_safe=true,~device="/dev/video0") + audio_pipeline = "autoaudiosrc" + video_pipeline = "v4l2src device=#{device}" + input.gstreamer.audio_video(id=id, clock_safe=clock_safe, audio_pipeline=audio_pipeline, video_pipeline=video_pipeline) +end + +def gstreamer.encode_x264_avi(fname, source) + output.gstreamer.video(pipeline="videoconvert ! x264enc ! avimux ! filesink location=\"#{fname}\"", source) +end + +def gstreamer.encode_jpeg_avi(fname, source) + output.gstreamer.video(pipeline="videoconvert ! jpegenc ! avimux ! filesink location=\"#{fname}\"", source) +end + +def gstreamer.encode_mp3(fname, source) + output.gstreamer.audio(pipeline="audioconvert ! lamemp3enc ! filesink location=\"#{fname}\"", source) +end +%endif \ No newline at end of file diff --git a/python_apps/pypo/liquidsoap_scripts/library/http.liq b/python_apps/pypo/liquidsoap_scripts/library/http.liq index 109baf41e..53644ce38 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/http.liq +++ b/python_apps/pypo/liquidsoap_scripts/library/http.liq @@ -13,18 +13,12 @@ def http_response(~protocol="HTTP/1.1", ~headers=[], ~data="") = status = http_codes[string_of(code)] - # Set content-length if needed and not set by the - # user. + # Set content-length and connection: close headers = - if data != "" and - not list.mem_assoc("Content-Length",headers) - then - list.append([("Content-Length", - "#{string.length(data)}")], - headers) - else - headers - end + list.append(headers, + [("Content-Length", "#{string.length(data)}"), + ("Connection", "close")]) + headers = list.map(fun (x) -> "#{fst(x)}: #{snd(x)}",headers) headers = string.concat(separator="\r\n",headers) # If no headers are provided, we should avoid diff --git a/python_apps/pypo/liquidsoap_scripts/library/pervasives.liq b/python_apps/pypo/liquidsoap_scripts/library/pervasives.liq index bd894766b..722138018 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/pervasives.liq +++ b/python_apps/pypo/liquidsoap_scripts/library/pervasives.liq @@ -4,4 +4,5 @@ %include "lastfm.liq" %include "flows.liq" %include "http.liq" -%include "video_text.liq" \ No newline at end of file +%include "video_text.liq" +%include "gstreamer.liq" diff --git a/python_apps/pypo/liquidsoap_scripts/library/utils.liq b/python_apps/pypo/liquidsoap_scripts/library/utils.liq index 3d82d8997..a35af94f3 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/utils.liq +++ b/python_apps/pypo/liquidsoap_scripts/library/utils.liq @@ -21,10 +21,14 @@ end # @param a Key to look for # @param l List of pairs (key,value) def list.mem_assoc(a,l) - v = list.assoc(a,l) - # We check for existence, since "" may indicate - # either a binding (a,"") or no binding.. - list.mem((a,v),l) + def f(cur, el) = + if not cur then + fst(el) == a + else + cur + end + end + list.fold(f, false, l) end # Remove a pair from an associative list @@ -164,8 +168,7 @@ def out(s) output.prefered(mksafe(s)) end -# Special track insensitive fallback that -# always skip current song before switching. +# Special track insensitive fallback that always skips current song before switching. # @category Source / Track Processing # @param ~input The input source # @param f The fallback source @@ -212,14 +215,17 @@ end # Simple crossfade. # @category Source / Track Processing # @param ~start_next Duration in seconds of the crossed end of track. -# @param ~fade_in Duration of the fade in for next track -# @param ~fade_out Duration of the fade out for previous track -# @param s The source to use -def crossfade(~id="",~start_next,~fade_in,~fade_out,s) +# @param ~fade_in Duration of the fade in for next track. +# @param ~fade_out Duration of the fade out for previous track. +# @param ~conservative Always prepare for a premature end-of-track. +# @param s The source to use. +def crossfade(~id="",~conservative=true, + ~start_next=5.,~fade_in=3.,~fade_out=3., + s) s = fade.in(duration=fade_in,s) s = fade.out(duration=fade_out,s) fader = fun (a,b) -> add(normalize=false,[b,a]) - cross(id=id,conservative=true,duration=start_next,fader,s) + cross(id=id,conservative=conservative,duration=start_next,fader,s) end # Append speech-synthesized tracks reading the metadata. @@ -242,8 +248,7 @@ def helium(s) end %endif -# Return true if process exited with 0 code. -# Command should return quickly. +# Return true if process exited with 0 code. Command should return quickly. # @category System # @param command Command to test def test_process(command) @@ -277,12 +282,9 @@ def url.split(uri) = end end -# Register a server/telnet command to -# update a source's metadata. Returns -# a new source, which will receive the -# updated metadata. It behaves just like -# the pre-1.0 insert_metadata() operator, -# i.e. insert key1="val1",key2="val2",... +# Register a server/telnet command to update a source's metadata. Returns +# a new source, which will receive the updated metadata. The command has +# the following format: insert key1="val1",key2="val2",... # @category Source / Track Processing # @param ~id Force the value of the source ID. def server.insert_metadata(~id="",s) = @@ -424,15 +426,15 @@ end # @param ~conservative Always prepare for a premature end-of-track. # @param ~default Transition used when no rule applies \ # (default: sequence). -# @param ~high Value, in dB, for loud sound level -# @param ~medium Value, in dB, for medium sound level +# @param ~high Value, in dB, for loud sound level. +# @param ~medium Value, in dB, for medium sound level. # @param ~margin Margin to detect sources that have too different \ # sound level for crossing. # @param s The input source. def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3., ~default=(fun (a,b) -> sequence([a, b])), ~high=-15., ~medium=-32., ~margin=4., - ~width=2.,~conservative=false,s) + ~width=2.,~conservative=true,s) fade.out = fade.out(type="sin",duration=fade_out) fade.in = fade.in(type="sin",duration=fade_in) add = fun (a,b) -> add(normalize=false,[b, a]) @@ -549,7 +551,18 @@ def playlist.reloadable(~id="",~random=false,~on_done={()},uri) if request.resolve(playlist) then playlist = request.filename(playlist) files = playlist.parse(playlist) - list.map(snd,files) + def file_request(el) = + meta = fst(el) + file = snd(el) + s = list.fold(fun (cur, el) -> + "#{cur},#{fst(el)}=#{string.escape(snd(el))}", "", meta) + if s == "" then + file + else + "annotate:#{s}:#{file}" + end + end + list.map(file_request,files) else log(label=id,"Couldn't read playlist: request resolution failed.") [] diff --git a/python_apps/pypo/liquidsoap_scripts/library/video_text.liq b/python_apps/pypo/liquidsoap_scripts/library/video_text.liq index b372a1f7b..8c017a93d 100644 --- a/python_apps/pypo/liquidsoap_scripts/library/video_text.liq +++ b/python_apps/pypo/liquidsoap_scripts/library/video_text.liq @@ -1,5 +1,6 @@ %ifdef video.add_text.gd # Add a scrolling line of text on video frames. +# @category Source / Video Processing # @param ~id Force the value of the source ID. # @param ~color Text color (in 0xRRGGBB format). # @param ~cycle Cycle text. @@ -22,6 +23,7 @@ end %ifdef video.add_text.sdl # Add a scrolling line of text on video frames. +# @category Source / Video Processing # @param ~id Force the value of the source ID. # @param ~color Text color (in 0xRRGGBB format). # @param ~cycle Cycle text. diff --git a/python_apps/pypo/liquidsoap_scripts/ls_script.liq b/python_apps/pypo/liquidsoap_scripts/ls_script.liq index 1f476b919..34e56786e 100644 --- a/python_apps/pypo/liquidsoap_scripts/ls_script.liq +++ b/python_apps/pypo/liquidsoap_scripts/ls_script.liq @@ -201,9 +201,6 @@ def append_dj_inputs(master_harbor_input_port, master_harbor_input_mount_point, dj_live = mksafe(audio_to_stereo(input.harbor(id="live_dj_harbor", dj_harbor_input_mount_point, port=dj_harbor_input_port, auth=check_dj_client, max=40., on_connect=live_dj_connect, on_disconnect=live_dj_disconnect))) - master_dj = rewrite_metadata([("artist","Airtime"), ("title", "Master Dj")],master_dj) - dj_live = rewrite_metadata([("artist","Airtime"), ("title", "Live Dj")],dj_live) - ignore(output.dummy(master_dj, fallible=true)) ignore(output.dummy(dj_live, fallible=true)) switch(id="master_dj_switch", track_sensitive=false, transitions=[transition, transition, transition], [({!master_dj_enabled},master_dj), ({!live_dj_enabled},dj_live), ({true}, s)]) @@ -211,14 +208,12 @@ def append_dj_inputs(master_harbor_input_port, master_harbor_input_mount_point, master_dj = mksafe(audio_to_stereo(input.harbor(id="master_harbor", master_harbor_input_mount_point, port=master_harbor_input_port, auth=check_master_dj_client, max=40., on_connect=master_dj_connect, on_disconnect=master_dj_disconnect))) ignore(output.dummy(master_dj, fallible=true)) - master_dj = rewrite_metadata([("artist","Airtime"), ("title", "Master Dj")],master_dj) + switch(id="master_dj_switch", track_sensitive=false, transitions=[transition, transition], [({!master_dj_enabled},master_dj), ({true}, s)]) elsif dj_harbor_input_port != 0 and dj_harbor_input_mount_point != "" then dj_live = mksafe(audio_to_stereo(input.harbor(id="live_dj_harbor", dj_harbor_input_mount_point, port=dj_harbor_input_port, auth=check_dj_client, max=40., on_connect=live_dj_connect, on_disconnect=live_dj_disconnect))) - dj_live = rewrite_metadata([("artist","Airtime"), ("title", "Live Dj")],dj_live) - ignore(output.dummy(dj_live, fallible=true)) switch(id="live_dj_switch", track_sensitive=false, transitions=[transition, transition], [({!live_dj_enabled},dj_live), ({true}, s)]) else diff --git a/python_apps/pypo/pyponotify.py b/python_apps/pypo/pyponotify.py index 446da8d49..9c2f1688c 100644 --- a/python_apps/pypo/pyponotify.py +++ b/python_apps/pypo/pyponotify.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import traceback """ Python part of radio playout (pypo) @@ -102,6 +103,24 @@ class Notify: logger.debug('# Calling server to update webstream data #') logger.debug('#################################################') response = self.api_client.notify_webstream_data(data, media_id) + logger.debug("Response: " + json.dumps(response)) + + def run_with_options(self, options): + if options.error and options.stream_id: + self.notify_liquidsoap_status(options.error, options.stream_id, options.time) + elif options.connect and options.stream_id: + self.notify_liquidsoap_status("OK", options.stream_id, options.time) + elif options.source_name and options.source_status: + self.notify_source_status(options.source_name, options.source_status) + elif options.webstream: + self.notify_webstream_data(options.webstream, options.media_id) + elif options.media_id: + self.notify_media_start_playing(options.media_id) + elif options.liquidsoap_started: + self.notify_liquidsoap_started() + else: + logger.debug("Unrecognized option in options(%s). Doing nothing" \ + % str(options)) if __name__ == '__main__': @@ -112,41 +131,9 @@ if __name__ == '__main__': print '#########################################' # initialize - if options.error and options.stream_id: - try: - n = Notify() - n.notify_liquidsoap_status(options.error, options.stream_id, options.time) - except Exception, e: - print e - elif options.connect and options.stream_id: - try: - n = Notify() - n.notify_liquidsoap_status("OK", options.stream_id, options.time) - except Exception, e: - print e - elif options.source_name and options.source_status: - try: - n = Notify() - n.notify_source_status(options.source_name, options.source_status) - except Exception, e: - print e - elif options.webstream: - try: - n = Notify() - n.notify_webstream_data(options.webstream, options.media_id) - except Exception, e: - print e - elif options.media_id: - - try: - n = Notify() - n.notify_media_start_playing(options.media_id) - except Exception, e: - print e - elif options.liquidsoap_started: - try: - n = Notify() - n.notify_liquidsoap_started() - except Exception, e: - print e + try: + n = Notify() + n.run_with_options(options) + except Exception as e: + print( traceback.format_exc() ) diff --git a/utils/airtime-import/airtime-import.py b/utils/airtime-import/airtime-import.py index 5f1d305ca..c77a0f21e 100644 --- a/utils/airtime-import/airtime-import.py +++ b/utils/airtime-import/airtime-import.py @@ -21,7 +21,7 @@ logger.addHandler(ch) if (os.geteuid() != 0): print 'Must be a root user.' sys.exit() - + # loading config file try: config = ConfigObj('/etc/airtime/media-monitor.cfg') @@ -66,12 +66,12 @@ def copy_or_move_files_to(paths, dest, flag): print "Cannot find file or path: %s" % path except Exception as e: print "Error: ", e - + def format_dir_string(path): if(path[-1] != '/'): path = path+'/' return path - + def helper_get_stor_dir(): res = api_client.list_all_watched_dirs() if(res is None): @@ -87,18 +87,18 @@ def checkOtherOption(args): for i in args: if(i[0] == '-'): return True - + def errorIfMultipleOption(args, msg=''): if(checkOtherOption(args)): if(msg != ''): raise OptionValueError(msg) else: raise OptionValueError("This option cannot be combined with other options") - + def printHelp(): storage_dir = helper_get_stor_dir() if(storage_dir is None): - storage_dir = "Unknown" + storage_dir = "Unknown" else: storage_dir += "imported/" print """ @@ -111,15 +111,15 @@ There are two ways to import audio files into Airtime: Copied or moved files will be placed into the folder: %s - + Files will be automatically organized into the structure "Artist/Album/TrackNumber-TrackName-Bitrate.file_extension". 2) Use airtime-import to add a folder to the Airtime library ("watch" a folder). - + All the files in the watched folder will be imported to Airtime and the folder will be monitored to automatically detect any changes. Hence any - changes done in the folder(add, delete, edit a file) will trigger + changes done in the folder(add, delete, edit a file) will trigger updates in Airtime library. """ % storage_dir parser.print_help() @@ -159,6 +159,7 @@ def WatchAddAction(option, opt, value, parser): path = currentDir+path path = apc.encode_to(path, 'utf-8') if(os.path.isdir(path)): + os.chmod(path, 0765) res = api_client.add_watched_dir(path) if(res is None): exit("Unable to connect to the server.") @@ -209,7 +210,7 @@ def WatchRemoveAction(option, opt, value, parser): print "Removing the watch folder failed: %s" % res['msg']['error'] else: print "The given path is not a directory: %s" % path - + def StorageSetAction(option, opt, value, parser): bypass = False isF = '-f' in parser.rargs @@ -231,12 +232,12 @@ def StorageSetAction(option, opt, value, parser): confirm = confirm or 'N' if(confirm == 'n' or confirm =='N'): sys.exit(1) - + if(len(parser.rargs) > 1): raise OptionValueError("Too many arguments. This option requires exactly one argument.") elif(len(parser.rargs) == 0 ): raise OptionValueError("No argument found. This option requires exactly one argument.") - + path = parser.rargs[0] if (path[0] == "/" or path[0] == "~"): path = os.path.realpath(path) @@ -254,17 +255,17 @@ def StorageSetAction(option, opt, value, parser): print "Setting storage folder failed: %s" % res['msg']['error'] else: print "The given path is not a directory: %s" % path - + def StorageGetAction(option, opt, value, parser): errorIfMultipleOption(parser.rargs) if(len(parser.rargs) > 0): raise OptionValueError("This option does not take any arguments.") print helper_get_stor_dir() - + class OptionValueError(RuntimeError): def __init__(self, msg): self.msg = msg - + usage = """[-c|--copy FILE/DIR [FILE/DIR...]] [-m|--move FILE/DIR [FILE/DIR...]] [--watch-add DIR] [--watch-list] [--watch-remove DIR] [--storage-dir-set DIR] [--storage-dir-get]""" @@ -293,7 +294,7 @@ if('-h' in sys.argv): if(len(sys.argv) == 1 or '-' not in sys.argv[1]): printHelp() sys.exit() - + try: (option, args) = parser.parse_args() except Exception, e: @@ -306,7 +307,7 @@ except Exception, e: except SystemExit: printHelp() sys.exit() - + if option.help: printHelp() sys.exit() diff --git a/widgets/css/airtime-widgets.css b/widgets/css/airtime-widgets.css index 7ff364ead..29527cc39 100644 --- a/widgets/css/airtime-widgets.css +++ b/widgets/css/airtime-widgets.css @@ -1,213 +1,213 @@ -@charset "utf-8"; -/* CSS Document */ - - -#headerLiveHolder { - overflow: hidden; - position:relative; - width:300px!important; - font-family: Arial,Helvetica,sans-serif; - font-size: 13px; - margin: 0; - padding: 0; - } - #headerLiveHolder * { - margin: 0; - padding: 0; - } - #headerLiveHolder ul { - list-style-type:none; - margin: 0; - padding: 0; - } - #headerLiveHolder h4 { - font-size:10px; - color:#68bd44; - text-transform:uppercase; - font-weight:normal; - line-height:12px; - } - #headerLiveHolder ul li { - overflow:hidden; - position:relative; - height:12px; - padding-right:70px; - font-size:11px; - color:#999999; - line-height:11px; - } - #headerLiveHolder ul li.current { - font-weight:bold; - color:#666666; - } - #headerLiveHolder ul li span { - position:absolute; - right:0px; - top:0px; - } - #headerLiveHolder ul li span#time-elapsed { - right: 50px; - } - #headerLiveHolder ul li span#time-elapsed:before { - content: "e."; - } - #headerLiveHolder ul li span#time-remaining:before { - content: "r."; - } - -#onAirToday { - width:300px; - margin-bottom:15px; - border:1px solid #999999; - font-family: Arial,Helvetica,sans-serif; - } - #onAirToday h3 { - margin:0; - padding:0 10px; - font-size:13px; - color:#ffffff; - line-height:28px; - background: #999999; - } - #onAirToday table { - border-collapse: collapse; - clear: both; - padding: 0; - } - #onAirToday table tbody { - color: #000; - font-size:12px; - } - #onAirToday table tbody tr { - height: 30px; - } - #onAirToday table tbody tr td { - border-bottom: 1px dotted #d9d9d9; - height: 30px; - padding-left: 10px; - font-weight:bold; - } - #onAirToday table tbody tr td a { - text-decoration:none; - } - #onAirToday table tbody tr td a:hover { - text-decoration:underline; - } - #onAirToday table tbody tr:last-child td { - border-bottom: none; - } - #onAirToday table tbody tr td.time { - font-weight:normal; - color: #666666; - } -/* = PROGRAM SCHEDULE ---------------- */ - - /* - + Center Column */ - #scheduleTabs { - font-family: Arial,Helvetica,sans-serif; - } - #scheduleTabs ul { - overflow:hidden; - height:28px; - background: url( widget-img/schedule-tabs-list-bgr.png) repeat-x left bottom; - list-style-type:none; - margin:0; - padding:0; - } - #scheduleTabs ul li { - float:left; - height:28px; - margin:0; - padding:0; - } - #scheduleTabs ul li a { - display:block; - float:left; - height:25px; - padding:0 10px; - margin-top:3px; - font-size:12px; - font-weight:bold; - color:#666; - line-height:25px; - border-right:1px solid #dbdbdb; - border-left:1px solid #f4f4f4; - text-decoration:none; - } - #scheduleTabs ul li a:hover { - color:#000; - text-decoration:none; - } - #scheduleTabs ul li.ui-tabs-selected a { - height:27px; - margin-top:0px; - line-height:27px; - color:#333333; - background:#ffffff; - border:1px solid #cac9c9; border-bottom:none; - } - #scheduleTabs table { - clear:both; - padding:0px; - border-collapse:collapse; - width:100%; - } - #scheduleTabs table thead { - height:35px; - font-size:12px; color:#333333; line-height:35px; - background-color: #fff; - background: -moz-linear-gradient(top, #ffffff 10%, #f4f4f4 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(10%, #ffffff), color-stop(100%, #f4f4f4)); - border-bottom:1px solid #d4d4d4; - } - #scheduleTabs table thead tr { - height:35px; - } - #scheduleTabs table thead tr td { - padding-left:10px; - font-size:11px; color:#999999; text-transform:uppercase; - } - #scheduleTabs table tfoot { - } - #scheduleTabs table tbody { - font-size:13px; color:#666666; font-weight:bold; - } - #scheduleTabs table tbody tr { - height:30px; - } - #scheduleTabs table tbody tr td { - height:30px; - padding-left:10px; - border-bottom:1px dotted #b3b3b3; - } - #scheduleTabs table tbody tr td h4 { - color:#333333; font-size:12px; - margin:0; - padding:0; - } - #scheduleTabs table tbody tr td ul { - background:none !important; - list-style-type:none; - } - #scheduleTabs table tbody tr td ul li { - float:left; - height:30px !important; - padding:0 2px; - font-size:12px; color:#cc0000; font-weight:normal; line-height:30px !important; - } - #scheduleTabs table tbody tr td ul li a { - height:30px !important; - margin:0px !important; padding:0px !important; - font-size:12px; color:#68BD44 !important; font-weight:normal !important; text-transform:uppercase; line-height:30px !important; - background:none !important; - border:none !important; - } - #scheduleTabs table tbody tr td ul li a:hover { - text-decoration:underline; - } - -#scheduleTabs.ui-tabs .ui-tabs-hide { - display: none; -} +@charset "utf-8"; +/* CSS Document */ + + +#headerLiveHolder { + overflow: hidden; + position:relative; + width:300px!important; + font-family: Arial,Helvetica,sans-serif; + font-size: 13px; + margin: 0; + padding: 0; + } + #headerLiveHolder * { + margin: 0; + padding: 0; + } + #headerLiveHolder ul { + list-style-type:none; + margin: 0; + padding: 0; + } + #headerLiveHolder h4 { + font-size:10px; + color:#68bd44; + text-transform:uppercase; + font-weight:normal; + line-height:12px; + } + #headerLiveHolder ul li { + overflow:hidden; + position:relative; + height:12px; + padding-right:70px; + font-size:11px; + color:#999999; + line-height:11px; + } + #headerLiveHolder ul li.current { + font-weight:bold; + color:#666666; + } + #headerLiveHolder ul li span { + position:absolute; + right:0px; + top:0px; + } + #headerLiveHolder ul li span#time-elapsed { + right: 50px; + } + #headerLiveHolder ul li span#time-elapsed:before { + content: "e."; + } + #headerLiveHolder ul li span#time-remaining:before { + content: "r."; + } + +#onAirToday { + width:300px; + margin-bottom:15px; + border:1px solid #999999; + font-family: Arial,Helvetica,sans-serif; + } + #onAirToday h3 { + margin:0; + padding:0 10px; + font-size:13px; + color:#ffffff; + line-height:28px; + background: #999999; + } + #onAirToday table { + border-collapse: collapse; + clear: both; + padding: 0; + } + #onAirToday table tbody { + color: #000; + font-size:12px; + } + #onAirToday table tbody tr { + height: 30px; + } + #onAirToday table tbody tr td { + border-bottom: 1px dotted #d9d9d9; + height: 30px; + padding-left: 10px; + font-weight:bold; + } + #onAirToday table tbody tr td a { + text-decoration:none; + } + #onAirToday table tbody tr td a:hover { + text-decoration:underline; + } + #onAirToday table tbody tr:last-child td { + border-bottom: none; + } + #onAirToday table tbody tr td.time { + font-weight:normal; + color: #666666; + } +/* = PROGRAM SCHEDULE ---------------- */ + + /* + + Center Column */ + #scheduleTabs { + font-family: Arial,Helvetica,sans-serif; + } + #scheduleTabs ul { + overflow:hidden; + height:28px; + background: url( widget-img/schedule-tabs-list-bgr.png) repeat-x left bottom; + list-style-type:none; + margin:0; + padding:0; + } + #scheduleTabs ul li { + float:left; + height:28px; + margin:0; + padding:0; + } + #scheduleTabs ul li a { + display:block; + float:left; + height:25px; + padding:0 10px; + margin-top:3px; + font-size:12px; + font-weight:bold; + color:#666; + line-height:25px; + border-right:1px solid #dbdbdb; + border-left:1px solid #f4f4f4; + text-decoration:none; + } + #scheduleTabs ul li a:hover { + color:#000; + text-decoration:none; + } + #scheduleTabs ul li.ui-tabs-selected a { + height:27px; + margin-top:0px; + line-height:27px; + color:#333333; + background:#ffffff; + border:1px solid #cac9c9; border-bottom:none; + } + #scheduleTabs table { + clear:both; + padding:0px; + border-collapse:collapse; + width:100%; + } + #scheduleTabs table thead { + height:35px; + font-size:12px; color:#333333; line-height:35px; + background-color: #fff; + background: -moz-linear-gradient(top, #ffffff 10%, #f4f4f4 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(10%, #ffffff), color-stop(100%, #f4f4f4)); + border-bottom:1px solid #d4d4d4; + } + #scheduleTabs table thead tr { + height:35px; + } + #scheduleTabs table thead tr td { + padding-left:10px; + font-size:11px; color:#999999; text-transform:uppercase; + } + #scheduleTabs table tfoot { + } + #scheduleTabs table tbody { + font-size:13px; color:#666666; font-weight:bold; + } + #scheduleTabs table tbody tr { + height:30px; + } + #scheduleTabs table tbody tr td { + height:30px; + padding-left:10px; + border-bottom:1px dotted #b3b3b3; + } + #scheduleTabs table tbody tr td h4 { + color:#333333; font-size:12px; + margin:0; + padding:0; + } + #scheduleTabs table tbody tr td ul { + background:none !important; + list-style-type:none; + } + #scheduleTabs table tbody tr td ul li { + float:left; + height:30px !important; + padding:0 2px; + font-size:12px; color:#cc0000; font-weight:normal; line-height:30px !important; + } + #scheduleTabs table tbody tr td ul li a { + height:30px !important; + margin:0px !important; padding:0px !important; + font-size:12px; color:#68BD44 !important; font-weight:normal !important; text-transform:uppercase; line-height:30px !important; + background:none !important; + border:none !important; + } + #scheduleTabs table tbody tr td ul li a:hover { + text-decoration:underline; + } + +#scheduleTabs.ui-tabs .ui-tabs-hide { + display: none; +} \ No newline at end of file diff --git a/widgets/sample_page.html b/widgets/sample_page.html index 94cc51396..cba9c37ed 100644 --- a/widgets/sample_page.html +++ b/widgets/sample_page.html @@ -1,57 +1,57 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> -<title>Airtime widgets - - - - - - - -

    "Now Playing" Widget

    -This widget displays what is currently playing: -
    -
    -
    -
    -
    -

    "Today's Program" Widget

    -This widget displays what is being played today: -
    -
    -
    -
    -
    -

    "Weekly Program" Widget

    -This widget displays all the shows for the entire week: -
    -
    -
    - - - + + + + +Airtime widgets + + + + + + + +

    "Now Playing" Widget

    +This widget displays what is currently playing: +
    +
    +
    +
    +
    +

    "Today's Program" Widget

    +This widget displays what is being played today: +
    +
    +
    +
    +
    +

    "Weekly Program" Widget

    +This widget displays all the shows for the entire week: +
    +
    +
    + + + diff --git a/widgets/widget_schedule.html b/widgets/widget_schedule.html index f7e8ae2de..66b02c546 100644 --- a/widgets/widget_schedule.html +++ b/widgets/widget_schedule.html @@ -1,1823 +1,1823 @@ - - - - -Airtime widgets - - - - - - - - - - - - - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimeProgram NameDetails
    00:00 - 00:30

    The Interview (EN)

    00:30 - 01:00

    The Interview (EN)

    00:30 - 01:00

    Applause (EN)

    01:00 - 01:30

    L'invité

    01:30 - 02:00

    Applaudissement (FR)

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    The Interview (EN)

    03:00 - 03:30

    Newslink

      -
    • -
    03:30 - 04:00

    Applause (EN)

    04:00 - 04:30

    L'invité

    04:30 - 05:00

    Applaudissement (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Well Body

    08:00 - 08:30

    L'info chez vous (FR)

    09:00 - 09:30

    Brèves - Music - Promo

    09:30 - 10:00

    Youth Connection

    09:30 - 10:00

    Growing Matters (EN)

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Well Body

    13:00 - 13:30

    L'info chez vous (FR)

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    17:00 - 17:30

    L'Info Chez-Vous

    18:30 - 19:00

    Youth Connection

    19:00 - 19:30

    L'info chez vous (FR)

    19:30 - 20:00

    Réseau jeunesse

    20:00 - 20:30

    Newslink (EN)

    20:30 - 21:00

    Well Body

    22:30 - 23:00

    Youth Connection

    23:30 - 00:00

    Réseau jeunesse

    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    01:00 - 01:30

    L'Info chez-vous

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      -
    • -
    03:30 - 04:00

    Well Body

    04:00 - 04:30

    L'info chez-vous (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Choices (EN)

    08:00 - 08:30

    L'info chez vous (FR)

    09:00 - 09:30

    Brèves - Music - Promo

    10:30 - 11:00

    Le Grenier

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Choices (EN)

    13:00 - 13:30

    L'info chez vous (FR)

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    16:30 - 17:00

    Applause (EN)

    17:00 - 17:30

    L'Info Chez-Vous

    19:00 - 19:30

    L'info chez vous (FR)

    19:30 - 20:00

    Le Grenier

    20:00 - 20:30

    Newslink (EN)

    20:30 - 21:00

    Choices (EN)

    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    00:30 - 01:00

    Applause (EN)

    01:00 - 01:30

    L'Info chez-vous

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      -
    • -
    03:30 - 04:00

    Choices (EN)

    04:00 - 04:30

    L'info chez-vous (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    08:00 - 08:30

    L'info chez vous (FR)

    08:30 - 09:00

    Fifty Fifty (FR)

    09:00 - 09:30

    Brèves - Music - Promo

    09:30 - 10:00

    Well Body

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    13:00 - 13:30

    L'info chez vous (FR)

    13:30 - 14:00

    Fifty Fifty (FR)

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    16:30 - 16:45

    The Citizen (EN)

    17:00 - 17:30

    L'Info Chez-Vous

    17:30 - 17:45

    Le Citoyen

    17:30 - 17:45

    Le Citoyen

    18:30 - 19:00

    Well Body

    19:00 - 19:30

    L'info chez vous (FR)

    20:00 - 20:30

    Newslink (EN)

    21:30 - 22:00

    Fifty Fifty (FR)

    22:00 - 22:15

    The Citizen (EN)

    22:30 - 23:00

    Well Body (EN)

    23:00 - 23:15

    Le Citoyen

    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    00:30 - 00:45

    The Citizen (EN)

    01:00 - 01:30

    L'Info chez-vous

    01:30 - 01:45

    Le Citoyen

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      -
    • -
    04:00 - 04:30

    L'info chez-vous (FR)

    04:30 - 05:00

    Fifty Fifty (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Growing Matters (EN)

    08:00 - 08:30

    L'info chez vous (FR)

    08:30 - 09:00

    Le Grenier

    09:00 - 09:30

    Brèves - Music - Promo

    09:30 - 10:00

    Choices (EN)

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Growing Matters (EN)

    13:00 - 13:30

    L'info chez vous (FR)

    13:30 - 14:00

    Le Grenier

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    16:30 - 17:00

    Pages of History (EN)

    17:00 - 17:30

    L'Info Chez-Vous

    17:30 - 04:00

    Pages d'histoire

    18:30 - 19:00

    Choices (EN)

    19:00 - 19:30

    L'info chez vous (FR)

    20:00 - 20:30

    Newslink (EN)

    20:30 - 21:00

    Growing Matters (EN)

    21:30 - 22:00

    Le Grenier

    22:00 - 22:30

    Pages of History (EN)

    22:30 - 23:00

    Choices (EN)

    23:00 - 09:30

    Pages d'histoire

    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    00:30 - 01:00

    Pages of History (EN)

    01:00 - 01:30

    L'Info chez-vous

    01:30 - 12:00

    Pages d'histoire

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      -
    • -
    03:30 - 04:00

    Growing Matters (EN)

    04:00 - 04:30

    L'info chez-vous (FR)

    04:30 - 05:00

    Le Grenier

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Youth Connection

    08:00 - 08:30

    L'info chez vous (FR)

    08:30 - 09:00

    Réseau jeunesse

    09:00 - 09:30

    Brèves - Music - Promo

    10:30 - 11:00

    Fifty Fifty (FR)

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Youth Connection

    13:00 - 13:30

    L'info chez vous (FR)

    13:30 - 14:00

    Réseau jeunesse

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    17:00 - 17:30

    L'Info Chez-Vous

    17:30 - 18:00

    Mano River Press (EN)

    19:00 - 19:30

    L'info chez vous (FR)

    19:30 - 20:00

    Réseau jeunesse

    20:00 - 20:30

    Newslink (EN)

    21:30 - 22:00

    Fifty Fifty (FR)

    22:30 - 23:00

    Youth Connection

    23:30 - 00:00

    Réseau jeunesse

    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    01:00 - 01:30

    L'Info chez-vous

    03:00 - 03:30

    Newslink

      -
    • -
    03:30 - 04:00

    Youth Connection

    04:00 - 04:30

    L'info chez-vous (FR)

    04:30 - 05:00

    Réseau jeunesse

    07:00 - 07:30

    Mano River Press (EN)

    07:30 - 08:00

    Weekend Review (EN)

    08:30 - 09:00

    Focus (FR)

    09:15 - 09:30

    The Stadium (EN)

    09:30 - 10:00

    Echoes of Charles Taylor Trial

      -
    • -
    11:45 - 12:00

    The Stadium (EN)

    12:30 - 13:00

    Weekend Review (EN)

    12:30 - 13:00

    Mano River Press (EN)

    13:30 - 14:00

    Focus (FR)

    16:00 - 16:30

    Mano River Press (EN)

    16:30 - 17:00

    Weekend Review (EN)

    17:30 - 18:00

    Focus (FR)

    18:00 - 18:15

    The Stadium (EN)

    18:30 - 19:00

    Echoes of Charles Taylor Trial

      -
    • -
    20:00 - 20:30

    Mano River Press (EN)

    20:30 - 21:00

    Weekend Review (EN)

    21:30 - 22:00

    Focus (FR)

    22:00 - 22:15

    The Stadium (EN)

    22:30 - 23:00

    Echoes of Charles Taylor Trial

      -
    • -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimeProgram NameDetails
    00:00 - 00:30

    Mano River Press (EN)

    00:30 - 01:00

    Weekend Review (EN)

    01:30 - 02:00

    Focus (FR)

    03:00 - 03:30

    Mano River Press (EN)

    03:30 - 04:00

    Weekend Review (EN)

    04:30 - 05:00

    Focus (FR)

    07:00 - 07:30

    The Interview (EN)

    07:30 - 08:00

    Applause (EN)

    08:00 - 08:30

    L'invité

    08:30 - 09:00

    Applaudissement (FR)

    09:15 - 09:30

    The Citizen (EN)

    09:30 - 10:00

    Pages of History (EN)

    10:15 - 10:30

    Le Citoyen

    11:45 - 12:00

    The Citizen (EN)

    12:00 - 12:30

    The Interview (EN)

    12:30 - 13:00

    Applause (EN)

    13:00 - 13:30

    L'invité

    13:30 - 14:00

    Applaudissement (FR)

    16:00 - 16:30

    The Interview (EN)

    16:30 - 17:00

    Applause (EN)

    16:30 - 17:00

    The Interview (EN)

    17:00 - 17:30

    L'invité

    17:30 - 18:00

    Applaudissement (FR)

    18:00 - 18:15

    The Citizen (EN)

    18:30 - 19:00

    Pages of History (EN)

    19:00 - 19:15

    Le Citoyen

    20:00 - 20:30

    The Interview (EN)

    20:30 - 21:00

    Applause (EN)

    21:00 - 21:30

    L'invité

    21:30 - 22:00

    Applaudissement (FR)

    22:00 - 22:30

    The Interview (EN)

    22:00 - 22:30

    Pages of History (EN)

    -
    -
    - - + + + + +Airtime widgets + + + + + + + + + + + + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TimeProgram NameDetails
    00:00 - 00:30

    The Interview (EN)

    00:30 - 01:00

    The Interview (EN)

    00:30 - 01:00

    Applause (EN)

    01:00 - 01:30

    L'invité

    01:30 - 02:00

    Applaudissement (FR)

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    The Interview (EN)

    03:00 - 03:30

    Newslink

      +
    • +
    03:30 - 04:00

    Applause (EN)

    04:00 - 04:30

    L'invité

    04:30 - 05:00

    Applaudissement (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Well Body

    08:00 - 08:30

    L'info chez vous (FR)

    09:00 - 09:30

    Brèves - Music - Promo

    09:30 - 10:00

    Youth Connection

    09:30 - 10:00

    Growing Matters (EN)

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Well Body

    13:00 - 13:30

    L'info chez vous (FR)

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    17:00 - 17:30

    L'Info Chez-Vous

    18:30 - 19:00

    Youth Connection

    19:00 - 19:30

    L'info chez vous (FR)

    19:30 - 20:00

    Réseau jeunesse

    20:00 - 20:30

    Newslink (EN)

    20:30 - 21:00

    Well Body

    22:30 - 23:00

    Youth Connection

    23:30 - 00:00

    Réseau jeunesse

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    01:00 - 01:30

    L'Info chez-vous

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      +
    • +
    03:30 - 04:00

    Well Body

    04:00 - 04:30

    L'info chez-vous (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Choices (EN)

    08:00 - 08:30

    L'info chez vous (FR)

    09:00 - 09:30

    Brèves - Music - Promo

    10:30 - 11:00

    Le Grenier

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Choices (EN)

    13:00 - 13:30

    L'info chez vous (FR)

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    16:30 - 17:00

    Applause (EN)

    17:00 - 17:30

    L'Info Chez-Vous

    19:00 - 19:30

    L'info chez vous (FR)

    19:30 - 20:00

    Le Grenier

    20:00 - 20:30

    Newslink (EN)

    20:30 - 21:00

    Choices (EN)

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    00:30 - 01:00

    Applause (EN)

    01:00 - 01:30

    L'Info chez-vous

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      +
    • +
    03:30 - 04:00

    Choices (EN)

    04:00 - 04:30

    L'info chez-vous (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    08:00 - 08:30

    L'info chez vous (FR)

    08:30 - 09:00

    Fifty Fifty (FR)

    09:00 - 09:30

    Brèves - Music - Promo

    09:30 - 10:00

    Well Body

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    13:00 - 13:30

    L'info chez vous (FR)

    13:30 - 14:00

    Fifty Fifty (FR)

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    16:30 - 16:45

    The Citizen (EN)

    17:00 - 17:30

    L'Info Chez-Vous

    17:30 - 17:45

    Le Citoyen

    17:30 - 17:45

    Le Citoyen

    18:30 - 19:00

    Well Body

    19:00 - 19:30

    L'info chez vous (FR)

    20:00 - 20:30

    Newslink (EN)

    21:30 - 22:00

    Fifty Fifty (FR)

    22:00 - 22:15

    The Citizen (EN)

    22:30 - 23:00

    Well Body (EN)

    23:00 - 23:15

    Le Citoyen

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    00:30 - 00:45

    The Citizen (EN)

    01:00 - 01:30

    L'Info chez-vous

    01:30 - 01:45

    Le Citoyen

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      +
    • +
    04:00 - 04:30

    L'info chez-vous (FR)

    04:30 - 05:00

    Fifty Fifty (FR)

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Growing Matters (EN)

    08:00 - 08:30

    L'info chez vous (FR)

    08:30 - 09:00

    Le Grenier

    09:00 - 09:30

    Brèves - Music - Promo

    09:30 - 10:00

    Choices (EN)

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Growing Matters (EN)

    13:00 - 13:30

    L'info chez vous (FR)

    13:30 - 14:00

    Le Grenier

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    16:30 - 17:00

    Pages of History (EN)

    17:00 - 17:30

    L'Info Chez-Vous

    17:30 - 04:00

    Pages d'histoire

    18:30 - 19:00

    Choices (EN)

    19:00 - 19:30

    L'info chez vous (FR)

    20:00 - 20:30

    Newslink (EN)

    20:30 - 21:00

    Growing Matters (EN)

    21:30 - 22:00

    Le Grenier

    22:00 - 22:30

    Pages of History (EN)

    22:30 - 23:00

    Choices (EN)

    23:00 - 09:30

    Pages d'histoire

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    00:30 - 01:00

    Pages of History (EN)

    01:00 - 01:30

    L'Info chez-vous

    01:30 - 12:00

    Pages d'histoire

    02:00 - 02:30

    Music From Countries - Promo

    02:30 - 03:00

    Music From Countries - Promo

    03:00 - 03:30

    Newslink

      +
    • +
    03:30 - 04:00

    Growing Matters (EN)

    04:00 - 04:30

    L'info chez-vous (FR)

    04:30 - 05:00

    Le Grenier

    05:00 - 05:30

    Music From Countries - Promo

    05:30 - 06:00

    Music From Countries - Promo

    06:00 - 06:30

    Music From Countries - Promo

    06:30 - 07:00

    Music From Countries - Promo

    07:00 - 07:30

    Newslink (EN)

    07:30 - 08:00

    Youth Connection

    08:00 - 08:30

    L'info chez vous (FR)

    08:30 - 09:00

    Réseau jeunesse

    09:00 - 09:30

    Brèves - Music - Promo

    10:30 - 11:00

    Fifty Fifty (FR)

    11:00 - 12:00

    Breves-Music-Promo

    12:00 - 12:30

    Newslink (EN)

    12:30 - 13:00

    Youth Connection

    13:00 - 13:30

    L'info chez vous (FR)

    13:30 - 14:00

    Réseau jeunesse

    14:00 - 15:00

    Brèves - Music - Promo

    15:00 - 16:00

    Brèves - Music - Promo

    16:00 - 16:30

    Newslink

    17:00 - 17:30

    L'Info Chez-Vous

    17:30 - 18:00

    Mano River Press (EN)

    19:00 - 19:30

    L'info chez vous (FR)

    19:30 - 20:00

    Réseau jeunesse

    20:00 - 20:30

    Newslink (EN)

    21:30 - 22:00

    Fifty Fifty (FR)

    22:30 - 23:00

    Youth Connection

    23:30 - 00:00

    Réseau jeunesse

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TimeProgram NameDetails
    00:00 - 00:30

    Newslink (EN)

    01:00 - 01:30

    L'Info chez-vous

    03:00 - 03:30

    Newslink

      +
    • +
    03:30 - 04:00

    Youth Connection

    04:00 - 04:30

    L'info chez-vous (FR)

    04:30 - 05:00

    Réseau jeunesse

    07:00 - 07:30

    Mano River Press (EN)

    07:30 - 08:00

    Weekend Review (EN)

    08:30 - 09:00

    Focus (FR)

    09:15 - 09:30

    The Stadium (EN)

    09:30 - 10:00

    Echoes of Charles Taylor Trial

      +
    • +
    11:45 - 12:00

    The Stadium (EN)

    12:30 - 13:00

    Weekend Review (EN)

    12:30 - 13:00

    Mano River Press (EN)

    13:30 - 14:00

    Focus (FR)

    16:00 - 16:30

    Mano River Press (EN)

    16:30 - 17:00

    Weekend Review (EN)

    17:30 - 18:00

    Focus (FR)

    18:00 - 18:15

    The Stadium (EN)

    18:30 - 19:00

    Echoes of Charles Taylor Trial

      +
    • +
    20:00 - 20:30

    Mano River Press (EN)

    20:30 - 21:00

    Weekend Review (EN)

    21:30 - 22:00

    Focus (FR)

    22:00 - 22:15

    The Stadium (EN)

    22:30 - 23:00

    Echoes of Charles Taylor Trial

      +
    • +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TimeProgram NameDetails
    00:00 - 00:30

    Mano River Press (EN)

    00:30 - 01:00

    Weekend Review (EN)

    01:30 - 02:00

    Focus (FR)

    03:00 - 03:30

    Mano River Press (EN)

    03:30 - 04:00

    Weekend Review (EN)

    04:30 - 05:00

    Focus (FR)

    07:00 - 07:30

    The Interview (EN)

    07:30 - 08:00

    Applause (EN)

    08:00 - 08:30

    L'invité

    08:30 - 09:00

    Applaudissement (FR)

    09:15 - 09:30

    The Citizen (EN)

    09:30 - 10:00

    Pages of History (EN)

    10:15 - 10:30

    Le Citoyen

    11:45 - 12:00

    The Citizen (EN)

    12:00 - 12:30

    The Interview (EN)

    12:30 - 13:00

    Applause (EN)

    13:00 - 13:30

    L'invité

    13:30 - 14:00

    Applaudissement (FR)

    16:00 - 16:30

    The Interview (EN)

    16:30 - 17:00

    Applause (EN)

    16:30 - 17:00

    The Interview (EN)

    17:00 - 17:30

    L'invité

    17:30 - 18:00

    Applaudissement (FR)

    18:00 - 18:15

    The Citizen (EN)

    18:30 - 19:00

    Pages of History (EN)

    19:00 - 19:15

    Le Citoyen

    20:00 - 20:30

    The Interview (EN)

    20:30 - 21:00

    Applause (EN)

    21:00 - 21:30

    L'invité

    21:30 - 22:00

    Applaudissement (FR)

    22:00 - 22:30

    The Interview (EN)

    22:00 - 22:30

    Pages of History (EN)

    +
    +
    + + diff --git a/widgets/widgets.html b/widgets/widgets.html index 0c1ba7e2c..c4b8bc4dc 100644 --- a/widgets/widgets.html +++ b/widgets/widgets.html @@ -1,51 +1,51 @@ - - - - -Airtime widgets - - - - -
    -

    On air now >>

    -
      -
    • Current: Réseau jeunesse02:2227:37
    • - -
    -
    -
    -
    -
    -

    On air today

    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    04:30 - 05:00Réseau jeunesse
    07:00 - 07:30Mano River Press (EN)
    07:30 - 08:00Weekend Review (EN)
    08:30 - 09:00Focus (FR)
    09:15 - 09:30The Stadium (EN)
    09:30 - 10:00Echoes of Charles Taylor Trial
    -
    - - + + + + +Airtime widgets + + + + +
    +

    On air now >>

    +
      +
    • Current: Réseau jeunesse02:2227:37
    • + +
    +
    +
    +
    +
    +

    On air today

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    04:30 - 05:00Réseau jeunesse
    07:00 - 07:30Mano River Press (EN)
    07:30 - 08:00Weekend Review (EN)
    08:30 - 09:00Focus (FR)
    09:15 - 09:30The Stadium (EN)
    09:30 - 10:00Echoes of Charles Taylor Trial
    +
    + +