Merge branch '2.2.x' into 2.2.x-saas

This commit is contained in:
Martin Konecny 2012-11-06 16:11:51 -05:00
commit 3f7d8a0c7f
95 changed files with 6172 additions and 6094 deletions

29
CREDITS
View File

@ -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 CREDITS
======= =======

View File

@ -181,7 +181,8 @@ class LibraryController extends Zend_Controller_Action
} }
} }
} }
if ($isAdminOrPM) {
if ($isAdminOrPM || $file->getFileOwnerId() == $user->getId()) {
$menu["del"] = array("name"=> "Delete", "icon" => "delete", "url" => "/library/delete"); $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}"); $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(); $streams = array();
$message = null; $message = null;
$noPermissionMsg = "You don't have permission to delete selected items.";
foreach ($mediaItems as $media) { foreach ($mediaItems as $media) {
@ -293,19 +295,21 @@ class LibraryController extends Zend_Controller_Action
try { try {
Application_Model_Playlist::deletePlaylists($playlists, $user->getId()); Application_Model_Playlist::deletePlaylists($playlists, $user->getId());
} catch (PlaylistNoPermissionException $e) { } catch (PlaylistNoPermissionException $e) {
$this->view->message = "You don't have permission to delete selected items."; $message = $noPermissionMsg;
return;
} }
try { try {
Application_Model_Block::deleteBlocks($blocks, $user->getId()); Application_Model_Block::deleteBlocks($blocks, $user->getId());
} catch (BlockNoPermissionException $e) {
$message = $noPermissionMsg;
} catch (Exception $e) { } catch (Exception $e) {
//TODO: warn user that not all blocks could be deleted. //TODO: warn user that not all blocks could be deleted.
} }
try { try {
Application_Model_Webstream::deleteStreams($streams, $user->getId()); Application_Model_Webstream::deleteStreams($streams, $user->getId());
} catch (WebstreamNoPermissionException $e) {
$message = $noPermissionMsg;
} catch (Exception $e) { } catch (Exception $e) {
//TODO: warn user that not all streams could be deleted. //TODO: warn user that not all streams could be deleted.
Logging::info($e); Logging::info($e);
@ -317,7 +321,9 @@ class LibraryController extends Zend_Controller_Action
if (isset($file)) { if (isset($file)) {
try { try {
$res = $file->delete(true); $res = $file->delete();
} catch (FileNoPermissionException $e) {
$message = $noPermissionMsg;
} catch (Exception $e) { } catch (Exception $e) {
//could throw a scheduled in future exception. //could throw a scheduled in future exception.
$message = "Could not delete some scheduled files."; $message = "Could not delete some scheduled files.";
@ -364,15 +370,17 @@ class LibraryController extends Zend_Controller_Action
{ {
$user = Application_Model_User::getCurrentUser(); $user = Application_Model_User::getCurrentUser();
$isAdminOrPM = $user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER)); $isAdminOrPM = $user->isUserType(array(UTYPE_ADMIN, UTYPE_PROGRAM_MANAGER));
if (!$isAdminOrPM) {
return;
}
$request = $this->getRequest(); $request = $this->getRequest();
$form = new Application_Form_EditAudioMD();
$file_id = $this->_getParam('id', null); $file_id = $this->_getParam('id', null);
$file = Application_Model_StoredFile::Recall($file_id); $file = Application_Model_StoredFile::Recall($file_id);
if (!$isAdminOrPM && $file->getFileOwnerId() != $user->getId()) {
return;
}
$form = new Application_Form_EditAudioMD();
$form->populate($file->getDbColMetadata()); $form->populate($file->getDbColMetadata());
if ($request->isPost()) { if ($request->isPost()) {

View File

@ -513,6 +513,7 @@ class PlaylistController extends Zend_Controller_Action
} catch (BlockNotFoundException $e) { } catch (BlockNotFoundException $e) {
$this->playlistNotFound('block', true); $this->playlistNotFound('block', true);
} catch (Exception $e) { } catch (Exception $e) {
//Logging::info($e);
$this->playlistUnknownError($e); $this->playlistUnknownError($e);
} }
} }

View File

@ -15,7 +15,7 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
$defaultFade = Application_Model_Preference::GetDefaultFade(); $defaultFade = Application_Model_Preference::GetDefaultFade();
if ($defaultFade == "") { if ($defaultFade == "") {
$defaultFade = '0.500000'; $defaultFade = '0.5';
} }
//Station name //Station name
@ -37,8 +37,8 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
'required' => false, 'required' => false,
'filters' => array('StringTrim'), 'filters' => array('StringTrim'),
'validators' => array(array('regex', false, 'validators' => array(array('regex', false,
array('/^[0-9]{1,2}(\.\d{1,6})?$/', array('/^[0-9]{1,2}(\.\d{1})?$/',
'messages' => 'enter a time in seconds 0{.000000}'))), 'messages' => 'enter a time in seconds 0{.0}'))),
'value' => $defaultFade, 'value' => $defaultFade,
'decorators' => array( 'decorators' => array(
'ViewHelper' 'ViewHelper'

View File

@ -212,6 +212,14 @@ class Application_Form_SmartBlockCriteria extends Zend_Form_SubForm
}//for }//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 = new Zend_Form_Element_Select('sp_limit_options');
$limit->setAttrib('class', 'sp_input_select') $limit->setAttrib('class', 'sp_input_select')
->setDecorators(array('viewHelper')) ->setDecorators(array('viewHelper'))

View File

@ -308,10 +308,11 @@ SQL;
$length = $value." ".$modifier; $length = $value." ".$modifier;
} else { } else {
$hour = "00"; $hour = "00";
$mins = "00";
if ($modifier == "minutes") { if ($modifier == "minutes") {
if ($value >59) { if ($value >59) {
$hour = intval($value/60); $hour = intval($value/60);
$value = $value%60; $mins = $value%60;
} }
} elseif ($modifier == "hours") { } elseif ($modifier == "hours") {
@ -1092,6 +1093,14 @@ SQL;
->setDbValue($p_criteriaData['etc']['sp_limit_value']) ->setDbValue($p_criteriaData['etc']['sp_limit_value'])
->setDbBlockId($this->id) ->setDbBlockId($this->id)
->save(); ->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); $this->saveSmartBlockCriteria($p_criteria);
$insertList = $this->getListOfFilesUnderLimit(); $insertList = $this->getListOfFilesUnderLimit();
$this->deleteAllFilesFromBlock(); $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. // update length in playlist contents.
$this->updateBlockLengthInAllPlaylist(); $this->updateBlockLengthInAllPlaylist();
@ -1133,6 +1147,7 @@ SQL;
$info = $this->getListofFilesMeetCriteria(); $info = $this->getListofFilesMeetCriteria();
$files = $info['files']; $files = $info['files'];
$limit = $info['limit']; $limit = $info['limit'];
$repeat = $info['repeat_tracks'];
$insertList = array(); $insertList = array();
$totalTime = 0; $totalTime = 0;
@ -1141,20 +1156,38 @@ SQL;
// this moves the pointer to the first element in the collection // this moves the pointer to the first element in the collection
$files->getFirst(); $files->getFirst();
$iterator = $files->getIterator(); $iterator = $files->getIterator();
while ($iterator->valid() && $totalTime < $limit['time']) {
$isBlockFull = false;
while ($iterator->valid()) {
$id = $iterator->current()->getDbId(); $id = $iterator->current()->getDbId();
$length = Application_Common_DateHelper::calculateLengthInSeconds($iterator->current()->getDbLength()); $length = Application_Common_DateHelper::calculateLengthInSeconds($iterator->current()->getDbLength());
$insertList[$id] = $length; $insertList[] = array('id'=>$id, 'length'=>$length);
$totalTime += $length; $totalTime += $length;
$totalItems++; $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; break;
} }
$iterator->next(); $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; return $insertList;
} }
@ -1201,6 +1234,8 @@ SQL;
if ($criteria == "limit") { if ($criteria == "limit") {
$storedCrit["limit"] = array("value"=>$value, "modifier"=>$modifier); $storedCrit["limit"] = array("value"=>$value, "modifier"=>$modifier);
} else if($criteria == "repeat_tracks") {
$storedCrit["repeat_tracks"] = array("value"=>$value);
} else { } else {
$storedCrit["crit"][$criteria][] = array("criteria"=>$criteria, "value"=>$value, "modifier"=>$modifier, "extra"=>$extra, "display_name"=>$criteriaOptions[$criteria]); $storedCrit["crit"][$criteria][] = array("criteria"=>$criteria, "value"=>$value, "modifier"=>$modifier, "extra"=>$extra, "display_name"=>$criteriaOptions[$criteria]);
} }
@ -1316,6 +1351,7 @@ SQL;
} }
// construct limit restriction // construct limit restriction
$limits = array(); $limits = array();
if (isset($storedCrit['limit'])) { if (isset($storedCrit['limit'])) {
if ($storedCrit['limit']['modifier'] == "items") { if ($storedCrit['limit']['modifier'] == "items") {
$limits['time'] = 1440 * 60; $limits['time'] = 1440 * 60;
@ -1327,10 +1363,16 @@ SQL;
$limits['items'] = null; $limits['items'] = null;
} }
} }
$repeatTracks = 0;
if (isset($storedCrit['repeat_tracks'])) {
$repeatTracks = $storedCrit['repeat_tracks']['value'];
}
try { try {
$out = $qry->setFormatter(ModelCriteria::FORMAT_ON_DEMAND)->find(); $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) { } catch (Exception $e) {
Logging::info($e); Logging::info($e);
} }

View File

@ -13,9 +13,9 @@ class Application_Model_Datatables
if ($dbname == 'utime' || $dbname == 'mtime') { if ($dbname == 'utime' || $dbname == 'mtime') {
$input1 = isset($info[0])?Application_Common_DateHelper::ConvertToUtcDateTimeString($info[0]):null; $input1 = isset($info[0])?Application_Common_DateHelper::ConvertToUtcDateTimeString($info[0]):null;
$input2 = isset($info[1])?Application_Common_DateHelper::ConvertToUtcDateTimeString($info[1]):null; $input2 = isset($info[1])?Application_Common_DateHelper::ConvertToUtcDateTimeString($info[1]):null;
} else if($dbname == 'bit_rate') { } else if($dbname == 'bit_rate' || $dbname == 'sample_rate') {
$input1 = isset($info[0])?intval($info[0]) * 1000:null; $input1 = isset($info[0])?doubleval($info[0]) * 1000:null;
$input2 = isset($info[1])?intval($info[1]) * 1000:null; $input2 = isset($info[1])?doubleval($info[1]) * 1000:null;
} else { } else {
$input1 = isset($info[0])?$info[0]:null; $input1 = isset($info[0])?$info[0]:null;
$input2 = isset($info[1])?$info[1]:null; $input2 = isset($info[1])?$info[1]:null;

View File

@ -189,8 +189,8 @@ class Application_Model_Preference
$fade = self::getValue("default_fade"); $fade = self::getValue("default_fade");
if ($fade === "") { if ($fade === "") {
// the default value of the fade is 00.500000 // the default value of the fade is 00.5
return "00.500000"; return "00.5";
} }
// we need this function to work with 2.0 version on default_fade value in cc_pref // 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 = $out;
} }
$fade = number_format($fade, 6); $fade = number_format($fade, 2);
//fades need 2 leading zeros for DateTime conversion //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; return $fade;
} }

View File

@ -292,7 +292,8 @@ SQL;
ft.artist_name AS file_artist_name, ft.artist_name AS file_artist_name,
ft.album_title AS file_album_title, ft.album_title AS file_album_title,
ft.length AS file_length, ft.length AS file_length,
ft.file_exists AS file_exists ft.file_exists AS file_exists,
ft.mime AS file_mime
SQL; SQL;
$filesJoin = <<<SQL $filesJoin = <<<SQL
cc_schedule AS sched cc_schedule AS sched
@ -319,7 +320,8 @@ SQL;
sub.login AS file_artist_name, sub.login AS file_artist_name,
ws.description AS file_album_title, ws.description AS file_album_title,
ws.length AS file_length, ws.length AS file_length,
't'::BOOL AS file_exists 't'::BOOL AS file_exists,
NULL as file_mime
SQL; SQL;
$streamJoin = <<<SQL $streamJoin = <<<SQL
cc_schedule AS sched cc_schedule AS sched
@ -661,6 +663,7 @@ SQL;
$data["media"][$switch_start]['start'] = $switch_start; $data["media"][$switch_start]['start'] = $switch_start;
$data["media"][$switch_start]['end'] = $switch_start; $data["media"][$switch_start]['end'] = $switch_start;
$data["media"][$switch_start]['event_type'] = "switch_off"; $data["media"][$switch_start]['event_type'] = "switch_off";
$data["media"][$switch_start]['type'] = "event";
$data["media"][$switch_start]['independent_event'] = true; $data["media"][$switch_start]['independent_event'] = true;
} }
} }

View File

@ -105,6 +105,10 @@ class Application_Model_Scheduler
throw new Exception("You are not allowed to schedule show {$show->getDbName()}."); 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")); $showEndEpoch = floatval($instance->getDbEnds("U.u"));
if ($showEndEpoch < $nowEpoch) { if ($showEndEpoch < $nowEpoch) {
@ -366,10 +370,9 @@ class Application_Model_Scheduler
* @param array $fileIds * @param array $fileIds
* @param array $playlistIds * @param array $playlistIds
*/ */
private function insertAfter($scheduleItems, $schedFiles, $adjustSched = true) private function insertAfter($scheduleItems, $schedFiles, $adjustSched = true, $mediaItems = null)
{ {
try { try {
$affectedShowInstances = array(); $affectedShowInstances = array();
//dont want to recalculate times for moved items. //dont want to recalculate times for moved items.
@ -385,6 +388,16 @@ class Application_Model_Scheduler
foreach ($scheduleItems as $schedule) { foreach ($scheduleItems as $schedule) {
$id = intval($schedule["id"]); $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) { if ($id !== 0) {
$schedItem = CcScheduleQuery::create()->findPK($id, $this->con); $schedItem = CcScheduleQuery::create()->findPK($id, $this->con);
$instance = $schedItem->getCcShowInstances($this->con); $instance = $schedItem->getCcShowInstances($this->con);
@ -527,10 +540,32 @@ class Application_Model_Scheduler
$this->validateRequest($scheduleItems); $this->validateRequest($scheduleItems);
$requireDynamicContentCreation = false;
foreach ($mediaItems as $media) {
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) { foreach ($mediaItems as $media) {
$schedFiles = array_merge($schedFiles, $this->retrieveMediaFiles($media["id"], $media["type"])); $schedFiles = array_merge($schedFiles, $this->retrieveMediaFiles($media["id"], $media["type"]));
} }
$this->insertAfter($scheduleItems, $schedFiles, $adjustSched); $this->insertAfter($scheduleItems, $schedFiles, $adjustSched);
}
$this->con->commit(); $this->con->commit();

View File

@ -270,6 +270,13 @@ SQL;
try { try {
//update the status flag in cc_schedule. //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() $instances = CcShowInstancesQuery::create()
->filterByDbEnds($current_timestamp, Criteria::GREATER_THAN) ->filterByDbEnds($current_timestamp, Criteria::GREATER_THAN)
->filterByDbShowId($this->_showId) ->filterByDbShowId($this->_showId)
@ -1254,6 +1261,7 @@ SQL;
$con = Propel::getConnection(CcSchedulePeer::DATABASE_NAME); $con = Propel::getConnection(CcSchedulePeer::DATABASE_NAME);
$con->beginTransaction(); $con->beginTransaction();
//current timesamp in UTC. //current timesamp in UTC.
$current_timestamp = gmdate("Y-m-d H:i:s"); $current_timestamp = gmdate("Y-m-d H:i:s");

View File

@ -41,6 +41,7 @@ class Application_Model_ShowBuilder
"fadein" => "", "fadein" => "",
"fadeout" => "", "fadeout" => "",
"image" => false, "image" => false,
"mime" => null,
"color" => "", //in hex without the '#' sign. "color" => "", //in hex without the '#' sign.
"backgroundColor" => "", //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["cueout"] = $p_item["cue_out"];
$row["fadein"] = round(substr($p_item["fade_in"], 6), 6); $row["fadein"] = round(substr($p_item["fade_in"], 6), 6);
$row["fadeout"] = round(substr($p_item["fade_out"], 6), 6); $row["fadeout"] = round(substr($p_item["fade_out"], 6), 6);
$row["mime"] = $p_item["file_mime"];
$row["pos"] = $this->pos++; $row["pos"] = $this->pos++;

View File

@ -26,7 +26,10 @@ class Application_Model_Soundcloud
public function uploadTrack($filepath, $filename, $description, public function uploadTrack($filepath, $filename, $description,
$tags=array(), $release=null, $genre=null) $tags=array(), $release=null, $genre=null)
{ {
if ($this->getToken()) {
if (!$this->getToken()) {
throw new NoSoundCloundToken();
}
if (count($tags)) { if (count($tags)) {
$tags = join(" ", $tags); $tags = join(" ", $tags);
$tags = $tags." ".Application_Model_Preference::GetSoundCloudTags(); $tags = $tags." ".Application_Model_Preference::GetSoundCloudTags();
@ -82,9 +85,7 @@ class Application_Model_Soundcloud
); );
return $response; return $response;
} else {
throw new NoSoundCloundToken();
}
} }
public static function uploadSoundcloud($id) public static function uploadSoundcloud($id)

View File

@ -335,7 +335,7 @@ SQL;
* @param boolean $p_deleteFile * @param boolean $p_deleteFile
* *
*/ */
public function delete($deleteFromPlaylist=false) public function delete()
{ {
$filepath = $this->getFilePath(); $filepath = $this->getFilePath();
@ -344,6 +344,13 @@ SQL;
throw new DeleteScheduledFileException(); 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()); $music_dir = Application_Model_MusicDir::getDirByPK($this->_file->getDbDirectory());
$type = $music_dir->getType(); $type = $music_dir->getType();
@ -352,9 +359,6 @@ SQL;
Application_Model_RabbitMq::SendMessageToMediaMonitor("file_delete", $data); Application_Model_RabbitMq::SendMessageToMediaMonitor("file_delete", $data);
} }
if ($deleteFromPlaylist) {
Application_Model_Playlist::DeleteFileFromAllPlaylists($this->getId());
}
// set file_exists falg to false // set file_exists falg to false
$this->_file->setDbFileExists(false); $this->_file->setDbFileExists(false);
$this->_file->save(); $this->_file->save();
@ -1161,6 +1165,10 @@ SQL;
return $this->_file->getDbFileExists(); return $this->_file->getDbFileExists();
} }
public function getFileOwnerId()
{
return $this->_file->getDbOwnerId();
}
// note: never call this method from controllers because it does a sleep // note: never call this method from controllers because it does a sleep
public function uploadToSoundCloud() public function uploadToSoundCloud()
@ -1209,3 +1217,4 @@ SQL;
class DeleteScheduledFileException extends Exception {} class DeleteScheduledFileException extends Exception {}
class FileDoesNotExistException extends Exception {} class FileDoesNotExistException extends Exception {}
class FileNoPermissionException extends Exception {}

View File

@ -92,7 +92,7 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
if (count($leftOver) == 0) { if (count($leftOver) == 0) {
CcWebstreamQuery::create()->findPKs($p_ids)->delete(); CcWebstreamQuery::create()->findPKs($p_ids)->delete();
} else { } 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); $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); list($mime, $content_length_found) = self::discoverStreamMime($mediaUrl);
} }
} catch (Exception $e) { } catch (Exception $e) {
@ -307,7 +307,7 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
$media_url = self::getM3uUrl($url); $media_url = self::getM3uUrl($url);
} elseif (preg_match("/xspf\+xml/", $mime)) { } elseif (preg_match("/xspf\+xml/", $mime)) {
$media_url = self::getXspfUrl($url); $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); $media_url = self::getPlsUrl($url);
} elseif (preg_match("/(mpeg|ogg)/", $mime)) { } elseif (preg_match("/(mpeg|ogg)/", $mime)) {
if ($content_length_found) { if ($content_length_found) {
@ -370,3 +370,6 @@ class Application_Model_Webstream implements Application_Model_LibraryEditable
return $webstream->getDbId(); return $webstream->getDbId();
} }
} }
class WebstreamNoPermissionException extends Exception {}

View File

@ -54,7 +54,7 @@ class CcBlockTableMap extends TableMap {
*/ */
public function buildRelations() 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('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('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); $this->addRelation('CcBlockcriteria', 'CcBlockcriteria', RelationMap::ONE_TO_MANY, array('id' => 'block_id', ), 'CASCADE', null);

View File

@ -64,7 +64,7 @@ class CcSubjsTableMap extends TableMap {
$this->addRelation('CcPerms', 'CcPerms', RelationMap::ONE_TO_MANY, array('id' => 'subj', ), 'CASCADE', null); $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('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('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('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('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); $this->addRelation('CcSubjsToken', 'CcSubjsToken', RelationMap::ONE_TO_MANY, array('id' => 'user_id', ), 'CASCADE', null);

View File

@ -407,6 +407,9 @@ abstract class BaseCcSubjsPeer {
// Invalidate objects in CcPlaylistPeer instance pool, // Invalidate objects in CcPlaylistPeer instance pool,
// since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule. // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
CcPlaylistPeer::clearInstancePool(); 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, // Invalidate objects in CcPrefPeer instance pool,
// since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule. // since one or more of them may be deleted by ON DELETE CASCADE/SETNULL rule.
CcPrefPeer::clearInstancePool(); CcPrefPeer::clearInstancePool();

View File

@ -30,8 +30,22 @@
<dd id='sp_criteria-element' class='criteria-element'> <dd id='sp_criteria-element' class='criteria-element'>
<?php for ($i = 0; $i < $this->criteriasLength; $i++) {?> <?php for ($i = 0; $i < $this->criteriasLength; $i++) {?>
<?php for ($j = 0; $j < $this->modRowMap[$i]; $j++) {?> <?php for ($j = 0; $j < $this->modRowMap[$i]; $j++) {
<div <?php if (($i > 0) && ($this->element->getElement("sp_criteria_field_".$i."_".$j)->getAttrib('disabled') == 'disabled')) { 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;
?>
<div <?php if (($i > 0) && $disabled) {
echo 'style=display:none'; echo 'style=display:none';
} ?>> } ?>>
<?php echo $this->element->getElement("sp_criteria_field_".$i."_".$j) ?> <?php echo $this->element->getElement("sp_criteria_field_".$i."_".$j) ?>
@ -45,6 +59,9 @@
<a style='margin-right:3px' class='btn btn-small btn-danger' id='criteria_remove_<?php echo $i ?>'> <a style='margin-right:3px' class='btn btn-small btn-danger' id='criteria_remove_<?php echo $i ?>'>
<i class='icon-white icon-remove'></i> <i class='icon-white icon-remove'></i>
</a> </a>
<span class='db-logic-label' <?php if ($nextDisabled) echo "style='display:none'"?>>
<?php echo $logicLabel;?>
</span>
<?php if($this->element->getElement("sp_criteria_field_".$i."_".$j)->hasErrors()) : ?> <?php if($this->element->getElement("sp_criteria_field_".$i."_".$j)->hasErrors()) : ?>
<?php foreach($this->element->getElement("sp_criteria_field_".$i."_".$j)->getMessages() as $error): ?> <?php foreach($this->element->getElement("sp_criteria_field_".$i."_".$j)->getMessages() as $error): ?>
<span class='errors sp-errors'> <span class='errors sp-errors'>
@ -59,6 +76,20 @@
<br /> <br />
</dd> </dd>
<dd id='sp_repeate_tracks-element'>
<span class='sp_text_font'><?php echo $this->element->getElement('sp_repeat_tracks')->getLabel() ?></span>
<span class='repeat_tracks_help_icon'></span>
<?php echo $this->element->getElement('sp_repeat_tracks')?>
<?php if($this->element->getElement("sp_repeat_tracks")->hasErrors()) : ?>
<?php foreach($this->element->getElement("sp_repeat_tracks")->getMessages() as $error): ?>
<span class='errors sp-errors'>
<?php echo $error; ?>
</span>
<?php endforeach; ?>
<?php endif; ?>
<br />
</dd>
<dd id='sp_limit-element'> <dd id='sp_limit-element'>
<span class='sp_text_font'><?php echo $this->element->getElement('sp_limit_value')->getLabel() ?></span> <span class='sp_text_font'><?php echo $this->element->getElement('sp_limit_value')->getLabel() ?></span>
<?php echo $this->element->getElement('sp_limit_value')?> <?php echo $this->element->getElement('sp_limit_value')?>

View File

@ -1,6 +1,6 @@
<div class="ui-widget ui-widget-content block-shadow simple-formblock clearfix padded-strong"> <div class="ui-widget ui-widget-content block-shadow simple-formblock clearfix padded-strong">
<h2>Edit Metadata</h2> <h2>Edit Metadata</h2>
<?php $this->form->setAction($this->url()); <?php //$this->form->setAction($this->url());
echo $this->form; ?> echo $this->form; ?>
</div> </div>

View File

@ -1,5 +1,5 @@
<div id="import_status" class="library_import" style="display:none">File import in progress... <img src="/css/images/file_import_loader.gif"></img></div> <div id="import_status" class="library_import" style="display:none">File import in progress... <img src="/css/images/file_import_loader.gif"></img></div>
<fieldset class="toggle" id="filter_options"> <fieldset class="toggle closed" id="filter_options">
<legend style="cursor: pointer;"><span class="ui-icon ui-icon-triangle-2-n-s"></span>Advanced Search Options</legend> <legend style="cursor: pointer;"><span class="ui-icon ui-icon-triangle-2-n-s"></span>Advanced Search Options</legend>
<div id="advanced_search" class="advanced_search form-horizontal"></div> <div id="advanced_search" class="advanced_search form-horizontal"></div>
</fieldset> </fieldset>

View File

@ -92,5 +92,13 @@ if ($item['type'] == 2) {
<?php endforeach; ?> <?php endforeach; ?>
<?php else : ?> <?php else : ?>
<li class="spl_empty">Empty playlist</li> <li class="spl_empty">
<?php
if ($this->obj instanceof Application_Model_Block) {
echo 'Empty smart block';
} else {
echo 'Empty playlist';
}
?>
</li>
<?php endif; ?> <?php endif; ?>

View File

@ -1,7 +1,7 @@
<div class="wrapper"> <div class="wrapper">
<div id="library_content" class="lib-content tabs ui-widget ui-widget-content block-shadow alpha-block padded"> <div id="library_content" class="lib-content tabs ui-widget ui-widget-content block-shadow alpha-block padded">
<div id="import_status" style="display:none">File import in progress...</div> <div id="import_status" style="display:none">File import in progress...</div>
<fieldset class="toggle" id="filter_options"> <fieldset class="toggle closed" id="filter_options">
<legend style="cursor: pointer;"><span class="ui-icon ui-icon-triangle-2-n-s"></span>Advanced Search Options</legend> <legend style="cursor: pointer;"><span class="ui-icon ui-icon-triangle-2-n-s"></span>Advanced Search Options</legend>
<div id="advanced_search" class="advanced_search form-horizontal"></div> <div id="advanced_search" class="advanced_search form-horizontal"></div>
</fieldset> </fieldset>

View File

@ -9,6 +9,10 @@
<li id='lib-new-ws'><a href="#">New Webstream</a></li> <li id='lib-new-ws'><a href="#">New Webstream</a></li>
</ul> </ul>
</div> </div>
<div class="btn-group pull-right">
<button class="btn btn-inverse" type="submit" id="webstream_save" name="submit">Save</button>
</div>
<?php if (isset($this->obj)) : ?> <?php if (isset($this->obj)) : ?>
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<button id="ws_delete" class="btn" <?php if ($this->action == "new"): ?>style="display:none;"<?php endif; ?>aria-disabled="false">Delete</button> <button id="ws_delete" class="btn" <?php if ($this->action == "new"): ?>style="display:none;"<?php endif; ?>aria-disabled="false">Delete</button>
@ -37,6 +41,11 @@
<dd id="description-element"> <dd id="description-element">
<textarea cols="80" rows="24" id="description" name="description"><?php echo $this->obj->getDescription(); ?></textarea> <textarea cols="80" rows="24" id="description" name="description"><?php echo $this->obj->getDescription(); ?></textarea>
</dd> </dd>
</dl>
</fieldset>
<dl class="zend_form">
<dt id="submit-label" style="display: none;">&nbsp;</dt> <dt id="submit-label" style="display: none;">&nbsp;</dt>
<div id="url-error" class="errors" style="display:none;"></div> <div id="url-error" class="errors" style="display:none;"></div>
<dt id="streamurl-label"><label for="streamurl">Stream URL:</label></dt> <dt id="streamurl-label"><label for="streamurl">Stream URL:</label></dt>
@ -48,13 +57,7 @@
<dd id="streamlength-element"> <dd id="streamlength-element">
<input type="text" value="<?php echo $this->obj->getDefaultLength() ?>"/> <input type="text" value="<?php echo $this->obj->getDefaultLength() ?>"/>
</dd> </dd>
<dd id="submit-element" class="buttons">
<input class="btn btn-inverse" type="submit" value="Save" id="webstream_save" name="submit">
</dd>
</dl> </dl>
</fieldset>
<?php else : ?> <?php else : ?>
<div>No webstream</div> <div>No webstream</div>

View File

@ -250,7 +250,7 @@
<parameter name="foreign_table" value="cc_blockcontents" /> <parameter name="foreign_table" value="cc_blockcontents" />
<parameter name="expression" value="SUM(cliplength)" /> <parameter name="expression" value="SUM(cliplength)" />
</behavior> </behavior>
<foreign-key foreignTable="cc_subjs" name="cc_block_createdby_fkey"> <foreign-key foreignTable="cc_subjs" name="cc_block_createdby_fkey" onDelete="CASCADE">
<reference local="creator_id" foreign="id"/> <reference local="creator_id" foreign="id"/>
</foreign-key> </foreign-key>
</table> </table>

View File

@ -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_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; ALTER TABLE "cc_blockcontents" ADD CONSTRAINT "cc_blockcontents_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "cc_files" ("id") ON DELETE CASCADE;

View File

@ -105,7 +105,7 @@ select {
} }
.airtime_auth_help_icon, .custom_auth_help_icon, .stream_username_help_icon, .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; cursor: help;
position: relative; position: relative;
display:inline-block; zoom:1; display:inline-block; zoom:1;
@ -514,6 +514,9 @@ table.library-get-file-md.table-small{
/***** SMART BLOCK SPECIFIC STYLES BEGIN *****/ /***** SMART BLOCK SPECIFIC STYLES BEGIN *****/
.db-logic-label{
font-size:11px;
}
.sp-invisible{ .sp-invisible{
visibility: hidden; visibility: hidden;
} }

View File

@ -14,5 +14,6 @@ function isAudioSupported(mime){
//Note that checking the navigator.mimeTypes value does not work for IE7, but the alternative //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.... //is adding a javascript library to do the work for you, which seems like overkill....
return (!!audio.canPlayType && audio.canPlayType(bMime) != "") || 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);
} }

View File

@ -54,7 +54,10 @@ function open_audio_preview(type, id, audioFileTitle, audioFileArtist) {
audioFileTitle = audioFileTitle.substring(0,index); 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(); _preview_window.focus();
} }

View File

@ -37,6 +37,11 @@ var AIRTIME = (function(AIRTIME) {
var $nRow = $(nRow); var $nRow = $(nRow);
if (aData.ftype === "audioclip") { if (aData.ftype === "audioclip") {
$nRow.addClass("lib-audio"); $nRow.addClass("lib-audio");
$image = $nRow.find('td.library_type');
if (!isAudioSupported(aData.mime)) {
$image.html('<span class="ui-icon ui-icon-locked"></span>');
aData.image = '<span class="ui-icon ui-icon-locked"></span>';
}
} else if (aData.ftype === "stream") { } else if (aData.ftype === "stream") {
$nRow.addClass("lib-stream"); $nRow.addClass("lib-stream");
} else if (aData.ftype === "block") { } else if (aData.ftype === "block") {
@ -64,8 +69,9 @@ var AIRTIME = (function(AIRTIME) {
helper : function() { helper : function() {
var $el = $(this), selected = mod var $el = $(this), selected = mod
.getChosenAudioFilesLength(), container, message, li = $("#side_playlist ul[id='spl_sortable'] li:first"), width = li .getChosenAudioFilesLength(), container, message, li = $("#side_playlist ul[id='spl_sortable'] li:first"),
.width(), height = 55; width = li.width(), height = 55;
if (width > 798) width = 798;
// dragging an element that has an unselected // dragging an element that has an unselected
// checkbox. // checkbox.

View File

@ -29,6 +29,11 @@ var AIRTIME = (function(AIRTIME) {
if (aData.ftype === "audioclip") { if (aData.ftype === "audioclip") {
$nRow.addClass("lib-audio"); $nRow.addClass("lib-audio");
$image = $nRow.find('td.library_type');
if (!isAudioSupported(aData.mime)) {
$image.html('<span class="ui-icon ui-icon-locked"></span>');
aData.image = '<span class="ui-icon ui-icon-locked"></span>';
}
} else if (aData.ftype === "stream") { } else if (aData.ftype === "stream") {
$nRow.addClass("lib-stream"); $nRow.addClass("lib-stream");
} else { } else {

View File

@ -252,11 +252,11 @@ var AIRTIME = (function(AIRTIME) {
}; };
/* /*
* selects all items which the user can currently see. * selects all items which the user can currently see. (behaviour taken from
* (behaviour taken from gmail) * gmail)
* *
* by default the items are selected in reverse order * by default the items are selected in reverse order so we need to reverse
* so we need to reverse it back * it back
*/ */
mod.selectCurrentPage = function() { mod.selectCurrentPage = function() {
$.fn.reverse = [].reverse; $.fn.reverse = [].reverse;
@ -276,8 +276,8 @@ var AIRTIME = (function(AIRTIME) {
}; };
/* /*
* deselects all items that the user can currently see. * deselects all items that the user can currently see. (behaviour taken
* (behaviour taken from gmail) * from gmail)
*/ */
mod.deselectCurrentPage = function() { mod.deselectCurrentPage = function() {
var $inputs = $libTable.find("tbody input:checkbox"), var $inputs = $libTable.find("tbody input:checkbox"),
@ -433,7 +433,8 @@ var AIRTIME = (function(AIRTIME) {
oTable = $libTable.dataTable( { 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": [ "aoColumns": [
/* ftype */ { "sTitle" : "" , "mDataProp" : "ftype" , "bSearchable" : false , "bVisible" : false } , /* ftype */ { "sTitle" : "" , "mDataProp" : "ftype" , "bSearchable" : false , "bVisible" : false } ,
/* Checkbox */ { "sTitle" : "" , "mDataProp" : "checkbox" , "bSortable" : false , "bSearchable" : false , "sWidth" : "25px" , "sClass" : "library_checkbox" } , /* Checkbox */ { "sTitle" : "" , "mDataProp" : "checkbox" , "bSortable" : false , "bSearchable" : false , "sWidth" : "25px" , "sClass" : "library_checkbox" } ,
@ -524,11 +525,12 @@ var AIRTIME = (function(AIRTIME) {
"sAjaxDataProp": "files", "sAjaxDataProp": "files",
"fnServerData": function ( sSource, aoData, fnCallback ) { "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 * The real validation check is done in
* an action is performed in the Library page. * dataTables.columnFilter.js We also need to check it here
* In order for datatable to redraw the advanced search fields * because datatable is redrawn everytime an action is performed
* MUST all be valid. * 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 advSearchFields = $("div#advanced_search").children(':visible');
var advSearchValid = validateAdvancedSearch(advSearchFields); var advSearchValid = validateAdvancedSearch(advSearchFields);
@ -555,27 +557,36 @@ var AIRTIME = (function(AIRTIME) {
// 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(){ $(nRow).find('td.library_type').click(function(){
if (aData.ftype === 'playlist' && aData.length !== '0.0'){ 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); open_playlist_preview(playlistIndex, 0);
} else if (aData.ftype === 'audioclip') { } else if (aData.ftype === 'audioclip') {
if (isAudioSupported(aData.mime)) {
open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name); open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name);
}
} else if (aData.ftype == 'stream') { } else if (aData.ftype == 'stream') {
open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name); open_audio_preview(aData.ftype, aData.audioFile, aData.track_title, aData.artist_name);
} else if (aData.ftype == 'block' && aData.bl_type == 'static') { } 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); open_block_preview(blockIndex, 0);
} }
return false; return false;
}); });
alreadyclicked=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){ $(nRow).find('td:not(.library_checkbox, .library_type)').click(function(e){
var el=$(this); var el=$(this);
if (alreadyclicked) if (alreadyclicked)
{ {
alreadyclicked=false; // reset alreadyclicked=false; // reset
clearTimeout(alreadyclickedTimeout); // prevent this from happening clearTimeout(alreadyclickedTimeout); // prevent this
// from
// happening
// do what needs to happen on double click. // do what needs to happen on double click.
$tr = $(el).parent(); $tr = $(el).parent();
@ -596,7 +607,8 @@ var AIRTIME = (function(AIRTIME) {
return false; 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({ $(nRow).find("td:not(.library_checkbox, .library_type)").qtip({
content: { content: {
text: "Loading...", text: "Loading...",
@ -620,7 +632,8 @@ var AIRTIME = (function(AIRTIME) {
}, },
my: 'left center', my: 'left center',
at: 'right 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 effect: false // Disable positioning animation
}, },
style: { style: {
@ -641,7 +654,8 @@ var AIRTIME = (function(AIRTIME) {
// remove any selected nodes before the draw. // remove any selected nodes before the draw.
"fnPreDrawCallback": function( oSettings ) { "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(); $("#draggingContainer").remove();
}, },
"fnDrawCallback": AIRTIME.library.fnDrawCallback, "fnDrawCallback": AIRTIME.library.fnDrawCallback,
@ -674,13 +688,31 @@ var AIRTIME = (function(AIRTIME) {
setColumnFilter(oTable); setColumnFilter(oTable);
oTable.fnSetFilteringDelay(350); oTable.fnSetFilteringDelay(350);
var simpleSearchText;
$libContent.on("click", "legend", function(){ $libContent.on("click", "legend", function(){
$simpleSearch = $libContent.find("#library_display_filter label");
var $fs = $(this).parents("fieldset"); var $fs = $(this).parents("fieldset");
if ($fs.hasClass("closed")) { if ($fs.hasClass("closed")) {
$fs.removeClass("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 { 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"); $fs.addClass("closed");
} }
}); });
@ -796,12 +828,16 @@ var AIRTIME = (function(AIRTIME) {
callback = function() { callback = function() {
if (data.ftype === 'playlist' && data.length !== '0.0'){ 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); open_playlist_preview(playlistIndex, 0);
} else if (data.ftype === 'audioclip' || data.ftype === 'stream') { } else if (data.ftype === 'audioclip' || data.ftype === 'stream') {
open_audio_preview(data.ftype, data.audioFile, data.track_title, data.artist_name); open_audio_preview(data.ftype, data.audioFile, data.track_title, data.artist_name);
} else if (data.ftype === 'block') { } 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); open_block_preview(blockIndex, 0);
} }
}; };
@ -812,7 +848,8 @@ var AIRTIME = (function(AIRTIME) {
if (oItems.del !== undefined) { if (oItems.del !== undefined) {
// delete through the playlist controller, will reset // delete through the playlist controller, will reset
//playlist screen if this is the currently edited playlist. // playlist screen if this is the currently edited
// playlist.
if ((data.ftype === "playlist" || data.ftype === "block") && screen === "playlist") { if ((data.ftype === "playlist" || data.ftype === "block") && screen === "playlist") {
callback = function() { callback = function() {
aMedia = []; aMedia = [];
@ -985,7 +1022,8 @@ function addQtipToSCIcons(){
viewport: $(window) viewport: $(window)
}, },
show: { 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) viewport: $(window)
}, },
show: { 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")){ }else if($(this).hasClass("sc-error")){
@ -1039,7 +1078,8 @@ function addQtipToSCIcons(){
viewport: $(window) viewport: $(window)
}, },
show: { show: {
ready: true // Needed to make it show on first mouseover event ready: true // Needed to make it show on first mouseover
// event
} }
}); });
} }
@ -1101,11 +1141,11 @@ function validateAdvancedSearch(divs) {
addRemoveValidationIcons(valid, $(field), searchTermType); addRemoveValidationIcons(valid, $(field), searchTermType);
/* Empty fields should not have valid/invalid indicator /*
* Range values are considered valid even if only the * Empty fields should not have valid/invalid indicator Range values
* 'From' value is provided. Therefore, if the 'To' value * are considered valid even if only the 'From' value is provided.
* is empty but the 'From' value is not empty we need to * Therefore, if the 'To' value is empty but the 'From' value is not
* keep the validation icon on screen. * empty we need to keep the validation icon on screen.
*/ */
} else if (searchTerm[0] === "" && searchTerm[1] !== "" || } else if (searchTerm[0] === "" && searchTerm[1] !== "" ||
searchTerm[0] === "" && searchTerm[1] === ""){ searchTerm[0] === "" && searchTerm[1] === ""){
@ -1158,12 +1198,9 @@ function addRemoveValidationIcons(valid, field, searchTermType) {
} }
} }
/* Validation types: /*
* s => string * Validation types: s => string i => integer n => numeric (positive/negative,
* i => integer * whole/decimals) t => timestamp l => length
* n => numeric (positive/negative, whole/decimals)
* t => timestamp
* l => length
*/ */
var validationTypes = { var validationTypes = {
"album_title" : "s", "album_title" : "s",
@ -1192,7 +1229,7 @@ var validationTypes = {
"owner_id" : "s", "owner_id" : "s",
"rating" : "i", "rating" : "i",
"replay_gain" : "n", "replay_gain" : "n",
"sample_rate" : "i", "sample_rate" : "n",
"track_title" : "s", "track_title" : "s",
"track_number" : "i", "track_number" : "i",
"info_url" : "s", "info_url" : "s",

View File

@ -369,6 +369,9 @@ var AIRTIME = (function(AIRTIME){
$.each($("div .big_play"), function(index, value){ $.each($("div .big_play"), function(index, value){
if ($(value).attr('blockId') === undefined) { if ($(value).attr('blockId') === undefined) {
var mime = $(value).attr("data-mime-type"); var mime = $(value).attr("data-mime-type");
//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)) { if (isAudioSupported(mime)) {
$(value).bind("click", openAudioPreview); $(value).bind("click", openAudioPreview);
} else { } else {
@ -393,6 +396,7 @@ var AIRTIME = (function(AIRTIME){
}, },
}) })
} }
}
} else { } else {
if ($(value).attr('blocktype') === 'dynamic') { if ($(value).attr('blocktype') === 'dynamic') {
$(value).attr("class", "big_play_disabled dark_class"); $(value).attr("class", "big_play_disabled dark_class");

View File

@ -8,14 +8,17 @@ function setSmartBlockEvents() {
/********** ADD CRITERIA ROW **********/ /********** ADD CRITERIA ROW **********/
form.find('#criteria_add').live('click', function(){ form.find('#criteria_add').live('click', function(){
var div = $('dd[id="sp_criteria-element"]').children('div:visible:last').next(); var div = $('dd[id="sp_criteria-element"]').children('div:visible:last');
div.find('.db-logic-label').text('and').show();
div = div.next().show();
div.show();
div.children().removeAttr('disabled'); div.children().removeAttr('disabled');
div = div.next(); div = div.next();
if (div.length === 0) { if (div.length === 0) {
$(this).hide(); $(this).hide();
} }
appendAddButton(); appendAddButton();
appendModAddButton(); appendModAddButton();
removeButtonCheck(); removeButtonCheck();
@ -285,6 +288,11 @@ function reindexElements() {
var divs = $('#smart-block-form').find('div select[name^="sp_criteria_field"]').parent(), var divs = $('#smart-block-form').find('div select[name^="sp_criteria_field"]').parent(),
index = 0, index = 0,
modIndex = 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){ $.each(divs, function(i, div){
if (i > 0 && index < 26) { if (i > 0 && index < 26) {
@ -292,8 +300,14 @@ function reindexElements() {
* a modifier row * a modifier row
*/ */
if ($(div).find('select[name^="sp_criteria_field"]').hasClass('sp-invisible')) { 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++; modIndex++;
} else { } else {
if ($(div).is(':visible')) {
$(div).prev().find('.db-logic-label').text('and').show();
}
index++; index++;
modIndex = 0; modIndex = 0;
} }
@ -338,7 +352,7 @@ function setupUI() {
var plContents = $('#spl_sortable').children(); var plContents = $('#spl_sortable').children();
var shuffleButton = $('button[id="shuffle_button"]'); var shuffleButton = $('button[id="shuffle_button"]');
if (plContents.text() !== 'Empty playlist') { if (!plContents.hasClass('spl_empty')) {
if (shuffleButton.hasClass('ui-state-disabled')) { if (shuffleButton.hasClass('ui-state-disabled')) {
shuffleButton.removeClass('ui-state-disabled'); shuffleButton.removeClass('ui-state-disabled');
shuffleButton.removeAttr('disabled'); shuffleButton.removeAttr('disabled');
@ -384,6 +398,28 @@ function setupUI() {
at: "right center" 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) { function enableAndShowExtraField(valEle, index) {

View File

@ -339,10 +339,22 @@ 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() { mod.builderDataTable = function() {
$sbContent = $('#show_builder'); $sbContent = $('#show_builder');
$lib = $("#library_content"), $lib = $("#library_content"),
$sbTable = $sbContent.find('table'); $sbTable = $sbContent.find('table');
var isInitialized = false;
oSchedTable = $sbTable.dataTable( { oSchedTable = $sbTable.dataTable( {
"aoColumns": [ "aoColumns": [
@ -357,7 +369,8 @@ var AIRTIME = (function(AIRTIME){
/* cue in */ {"mDataProp": "cuein", "sTitle": "Cue In", "bVisible": false, "sClass": "sb-cue-in"}, /* 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"}, /* 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 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, "bJQueryUI": true,
@ -537,12 +550,17 @@ var AIRTIME = (function(AIRTIME){
$image = $nRow.find('td.sb-image'); $image = $nRow.find('td.sb-image');
//check if the file exists. //check if the file exists.
if (aData.image === true) { if (aData.image === true) {
$nRow.addClass("lib-audio");
if (!isAudioSupported(aData.mime)) {
$image.html('<span class="ui-icon ui-icon-locked"></span>');
} else {
$image.html('<img title="Track preview" src="/css/images/icon_audioclip.png"></img>') $image.html('<img title="Track preview" src="/css/images/icon_audioclip.png"></img>')
.click(function() { .click(function() {
open_show_preview(aData.instance, aData.pos); open_show_preview(aData.instance, aData.pos);
return false; return false;
}); });
} }
}
else { else {
$image.html('<span class="ui-icon ui-icon-alert"></span>'); $image.html('<span class="ui-icon ui-icon-alert"></span>');
$image.find(".ui-icon-alert").qtip({ $image.find(".ui-icon-alert").qtip({
@ -636,6 +654,17 @@ var AIRTIME = (function(AIRTIME){
$("#draggingContainer").remove(); $("#draggingContainer").remove();
}, },
"fnDrawCallback": function fnBuilderDrawCallback(oSettings, json) { "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, var wrapperDiv,
markerDiv, markerDiv,
$td, $td,
@ -966,13 +995,18 @@ var AIRTIME = (function(AIRTIME){
"<i class='icon-white icon-cut'></i></button></div>") "<i class='icon-white icon-cut'></i></button></div>")
.append("<div class='btn-group'>" + .append("<div class='btn-group'>" +
"<button title='Remove selected scheduled items' class='ui-state-disabled btn btn-small' disabled='disabled'>" + "<button title='Remove selected scheduled items' class='ui-state-disabled btn btn-small' disabled='disabled'>" +
"<i class='icon-white icon-trash'></i></button></div>") "<i class='icon-white icon-trash'></i></button></div>");
.append("<div class='btn-group'>" +
//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("<div class='btn-group'>" +
"<button title='Jump to the current playing track' class='ui-state-disabled btn btn-small' disabled='disabled'>" + "<button title='Jump to the current playing track' class='ui-state-disabled btn btn-small' disabled='disabled'>" +
"<i class='icon-white icon-step-forward'></i></button></div>") "<i class='icon-white icon-step-forward'></i></button></div>")
.append("<div class='btn-group'>" + .append("<div class='btn-group'>" +
"<button title='Cancel current show' class='ui-state-disabled btn btn-small btn-danger' disabled='disabled'>" + "<button title='Cancel current show' class='ui-state-disabled btn btn-small btn-danger' disabled='disabled'>" +
"<i class='icon-white icon-ban-circle'></i></button></div>"); "<i class='icon-white icon-ban-circle'></i></button></div>");
}
$toolbar.append($menu); $toolbar.append($menu);
$menu = undefined; $menu = undefined;
@ -1021,7 +1055,7 @@ var AIRTIME = (function(AIRTIME){
if (AIRTIME.button.isDisabled('icon-step-forward', true) === true) { if (AIRTIME.button.isDisabled('icon-step-forward', true) === true) {
return; return;
} }
/*
var $scroll = $sbContent.find(".dataTables_scrolling"), var $scroll = $sbContent.find(".dataTables_scrolling"),
scrolled = $scroll.scrollTop(), scrolled = $scroll.scrollTop(),
scrollingTop = $scroll.offset().top, scrollingTop = $scroll.offset().top,
@ -1029,6 +1063,8 @@ var AIRTIME = (function(AIRTIME){
currentTop = current.offset().top; currentTop = current.offset().top;
$scroll.scrollTop(currentTop - scrollingTop + scrolled); $scroll.scrollTop(currentTop - scrollingTop + scrolled);
*/
mod.jumpToCurrentTrack();
}); });
//delete overbooked tracks. //delete overbooked tracks.

View File

@ -133,7 +133,8 @@ AIRTIME = (function(AIRTIME) {
$builder.find(dateEndId).datepicker(oBaseDatePickerSettings); $builder.find(dateEndId).datepicker(oBaseDatePickerSettings);
$builder.find(timeEndId).timepicker(oBaseTimePickerSettings); $builder.find(timeEndId).timepicker(oBaseTimePickerSettings);
oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId, dateEndId, timeEndId); oRange = AIRTIME.utilities.fnGetScheduleRange(dateStartId, timeStartId,
dateEndId, timeEndId);
AIRTIME.showbuilder.fnServerData.start = oRange.start; AIRTIME.showbuilder.fnServerData.start = oRange.start;
AIRTIME.showbuilder.fnServerData.end = oRange.end; AIRTIME.showbuilder.fnServerData.end = oRange.end;
@ -144,7 +145,8 @@ AIRTIME = (function(AIRTIME) {
$libWrapper = $lib.find("#library_display_wrapper"); $libWrapper = $lib.find("#library_display_wrapper");
$libWrapper.prepend($libClose); $libWrapper.prepend($libClose);
$builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 95); $builder.find('.dataTables_scrolling').css("max-height",
widgetHeight - 95);
$builder.on("click", "#sb_submit", showSearchSubmit); $builder.on("click", "#sb_submit", showSearchSubmit);
@ -154,25 +156,26 @@ AIRTIME = (function(AIRTIME) {
// reset timestamp to redraw the cursors. // reset timestamp to redraw the cursors.
AIRTIME.showbuilder.resetTimestamp(); AIRTIME.showbuilder.resetTimestamp();
$lib.show() $lib.show().width(Math.floor(screenWidth * 0.48));
.width(Math.floor(screenWidth * 0.48));
$builder.width(Math.floor(screenWidth * 0.48)) $builder.width(Math.floor(screenWidth * 0.48)).find("#sb_edit")
.find("#sb_edit") .remove().end().find("#sb_date_start")
.remove() .css("margin-left", 0).end();
.end()
.find("#sb_date_start")
.css("margin-left", 0)
.end();
schedTable.fnDraw(); schedTable.fnDraw();
$.ajax( { $.ajax( {
url : "/usersettings/set-now-playing-screen-settings", url : "/usersettings/set-now-playing-screen-settings",
type : "POST", type : "POST",
data: {settings : {library : true}, format: "json"}, data : {
settings : {
library : true
},
format : "json"
},
dataType : "json", dataType : "json",
success: function(){} success : function() {
}
}); });
}); });
@ -180,13 +183,9 @@ AIRTIME = (function(AIRTIME) {
var schedTable = $("#show_builder_table").dataTable(); var schedTable = $("#show_builder_table").dataTable();
$lib.hide(); $lib.hide();
$builder.width(screenWidth) $builder.width(screenWidth).find(".sb-timerange").prepend(
.find(".sb-timerange") $toggleLib).find("#sb_date_start").css("margin-left", 30)
.prepend($toggleLib) .end().end();
.find("#sb_date_start")
.css("margin-left", 30)
.end()
.end();
$toggleLib.removeClass("ui-state-hover"); $toggleLib.removeClass("ui-state-hover");
schedTable.fnDraw(); schedTable.fnDraw();
@ -194,26 +193,35 @@ AIRTIME = (function(AIRTIME) {
$.ajax( { $.ajax( {
url : "/usersettings/set-now-playing-screen-settings", url : "/usersettings/set-now-playing-screen-settings",
type : "POST", type : "POST",
data: {settings : {library : false}, format: "json"}, data : {
settings : {
library : false
},
format : "json"
},
dataType : "json", dataType : "json",
success: function(){} success : function() {
}
}); });
}); });
$builder.find('legend').click(function(ev, item){ $builder.find('legend').click(
function(ev, item) {
if ($fs.hasClass("closed")) { if ($fs.hasClass("closed")) {
$fs.removeClass("closed"); $fs.removeClass("closed");
$builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 150); $builder.find('.dataTables_scrolling').css(
} "max-height", widgetHeight - 150);
else { } else {
$fs.addClass("closed"); $fs.addClass("closed");
// set defaults for the options. // set defaults for the options.
$fs.find('select').val(0); $fs.find('select').val(0);
$fs.find('input[type="checkbox"]').attr("checked", false); $fs.find('input[type="checkbox"]').attr("checked",
$builder.find('.dataTables_scrolling').css("max-height", widgetHeight - 110); false);
$builder.find('.dataTables_scrolling').css(
"max-height", widgetHeight - 110);
} }
}); });
@ -231,7 +239,8 @@ AIRTIME = (function(AIRTIME) {
$builder.on("change", '#sb_show_filter', function(ev) { $builder.on("change", '#sb_show_filter', function(ev) {
if ($(this).val() !== 0) { if ($(this).val() !== 0) {
$(ev.delegateTarget).find('#sb_my_shows').attr("checked", false); $(ev.delegateTarget).find('#sb_my_shows')
.attr("checked", false);
} }
showSearchSubmit(); showSearchSubmit();
@ -239,11 +248,8 @@ AIRTIME = (function(AIRTIME) {
}); });
function checkScheduleUpdates() { function checkScheduleUpdates() {
var data = {}, var data = {}, oTable = $('#show_builder_table').dataTable(), fn = oTable
oTable = $('#show_builder_table').dataTable(), .fnSettings().fnServerData, start = fn.start, end = fn.end;
fn = oTable.fnSettings().fnServerData,
start = fn.start,
end = fn.end;
data["format"] = "json"; data["format"] = "json";
data["start"] = start; data["start"] = start;

View File

@ -574,7 +574,7 @@
}) })
}(window.jQuery);/* ============================================================ }(window.jQuery);/* ============================================================
* bootstrap-dropdown.js v2.1.0 * bootstrap-dropdown.js v2.2.1
* http://twitter.github.com/bootstrap/javascript.html#dropdowns * http://twitter.github.com/bootstrap/javascript.html#dropdowns
* ============================================================ * ============================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
@ -675,8 +675,9 @@
} }
function clearMenus() { function clearMenus() {
getParent($(toggle)) $(toggle).each(function () {
.removeClass('open') getParent($(this)).removeClass('open')
})
} }
function getParent($this) { function getParent($this) {
@ -685,7 +686,7 @@
if (!selector) { if (!selector) {
selector = $this.attr('href') selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
} }
$parent = $(selector) $parent = $(selector)
@ -713,14 +714,15 @@
/* APPLY TO STANDARD DROPDOWN ELEMENTS /* APPLY TO STANDARD DROPDOWN ELEMENTS
* =================================== */ * =================================== */
$(function () { $(document)
$('html')
.on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) // menu options don't work on tablet so trying this hack for now:
$('body') // https://github.com/twitter/bootstrap/issues/4550
.on('click.dropdown touchstart.dropdown.data-api', '.dropdown', function (e) { e.stopPropagation() }) //.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('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) .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
})
}(window.jQuery);/* ========================================================= }(window.jQuery);/* =========================================================
* bootstrap-modal.js v2.1.0 * bootstrap-modal.js v2.1.0

View File

@ -190,7 +190,7 @@
} else if (th.attr('id') == "length") { } else if (th.attr('id') == "length") {
label = " hh:mm:ss.t"; label = " hh:mm:ss.t";
} else if (th.attr('id') == "sample_rate") { } else if (th.attr('id') == "sample_rate") {
label = " Hz"; label = " kHz";
} }
th.html(_fnRangeLabelPart(0)); th.html(_fnRangeLabelPart(0));

View File

@ -0,0 +1,2 @@
<!-- b bs -->

View File

@ -67,7 +67,7 @@
* Removing a watched directory and adding it again preserves playlists & shows with those files. * 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. * 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. * 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.&nbsp; Useful for royalty reporting purposes. * Broadcast Log - export play count of tracks within a given time range. Useful for royalty reporting purposes.
* Minor Improvements: * Minor Improvements:
* Ability to turn off the broadcast. * Ability to turn off the broadcast.
* Editing metadata in the library will update the metadata on disk. * Editing metadata in the library will update the metadata on disk.
@ -78,7 +78,7 @@
* Repeating shows default to "No End" * Repeating shows default to "No End"
* Ability to "View on Soundcloud" for recorded shows in the calendar * 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) * "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 * 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 * Show "Disk Full" message when trying to upload a file that wont fit on the disk
* Reduced CPU utilization for OGG streams * Reduced CPU utilization for OGG streams

View File

@ -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 \ libspeex-dev libspeexdsp-dev speex libladspa-ocaml-dev festival festival-dev \
libsamplerate-dev libxmlplaylist-ocaml-dev libxmlrpc-light-ocaml-dev libflac-dev \ libsamplerate-dev libxmlplaylist-ocaml-dev libxmlrpc-light-ocaml-dev libflac-dev \
libxml-dom-perl libxml-dom-xpath-perl patch autoconf libmp3lame-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' root = '/home/martin/src'
do_run('mkdir -p %s' % root) do_run('mkdir -p %s' % root)

View File

@ -27,11 +27,11 @@ code=`lsb_release -cs`
if [ "$dist" = "Debian" ]; then if [ "$dist" = "Debian" ]; then
set +e 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=$? returncode=$?
set -e set -e
if [ "$returncode" -ne "0" ]; then 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
fi fi

View File

@ -29,9 +29,9 @@ dist=`lsb_release -is`
code=`lsb_release -cs` code=`lsb_release -cs`
if [ "$dist" -eq "Debian" ]; then 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 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
fi fi

View File

@ -20,6 +20,11 @@ import traceback
AIRTIME_VERSION = "2.2.0" 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'): def to_unicode(obj, encoding='utf-8'):
if isinstance(obj, basestring): if isinstance(obj, basestring):
if not isinstance(obj, unicode): if not isinstance(obj, unicode):
@ -39,7 +44,7 @@ def convert_dict_value_to_utf8(md):
# Airtime API Client # 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 # This is a little hacky fix so that I don't have to pass the config object
# everywhere where AirtimeApiClient needs to be initialized # everywhere where AirtimeApiClient needs to be initialized
@ -422,15 +427,13 @@ class AirtimeApiClient():
def send_media_monitor_requests(self, action_list, dry=False): 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 Send a gang of media monitor events at a time. actions_list is a
of dictionaries where every dictionary is representing an action. Every list of dictionaries where every dictionary is representing an
action dict must contain a 'mode' key that says what kind of action it action. Every action dict must contain a 'mode' key that says
is and an optional 'is_record' key that says whether the show was what kind of action it is and an optional 'is_record' key that
recorded or not. The value of this key does not matter, only if it's says whether the show was recorded or not. The value of this key
present or not. does not matter, only if it's present or not.
""" """
logger = self.logger
try:
url = self.construct_url('reload_metadata_group') url = self.construct_url('reload_metadata_group')
# We are assuming that action_list is a list of dictionaries such # We are assuming that action_list is a list of dictionaries such
# that every dictionary represents the metadata of a file along # that every dictionary represents the metadata of a file along
@ -464,11 +467,6 @@ class AirtimeApiClient():
response = self.get_response_from_server(req) response = self.get_response_from_server(req)
response = json.loads(response) response = json.loads(response)
return response return response
except ValueError: raise
except Exception, e:
logger.error('Exception: %s', e)
logger.error("traceback: %s", traceback.format_exc())
raise
#returns a list of all db files for a given directory in JSON format: #returns a list of all db files for a given directory in JSON format:
#{"files":["path/to/file1", "path/to/file2"]} #{"files":["path/to/file1", "path/to/file2"]}

View File

@ -1,8 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import media.monitor.process as md import media.metadata.process as md
import re
from os.path import normpath 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
defs_loaded = False
def is_defs_loaded():
global defs_loaded
return defs_loaded
def load_definitions():
with md.metadata('MDATA_KEY_DURATION') as t: with md.metadata('MDATA_KEY_DURATION') as t:
t.default(u'0.0') t.default(u'0.0')
t.depends('length') t.depends('length')
@ -11,7 +20,8 @@ with md.metadata('MDATA_KEY_DURATION') as t:
with md.metadata('MDATA_KEY_MIME') as t: with md.metadata('MDATA_KEY_MIME') as t:
t.default(u'') t.default(u'')
t.depends('mime') t.depends('mime')
t.translate(lambda k: k['mime'].replace('-','/')) # Is this necessary?
t.translate(lambda k: k['mime'].replace('audio/vorbis','audio/ogg'))
with md.metadata('MDATA_KEY_BITRATE') as t: with md.metadata('MDATA_KEY_BITRATE') as t:
t.default(u'') t.default(u'')
@ -23,7 +33,7 @@ with md.metadata('MDATA_KEY_SAMPLERATE') as t:
t.depends('sample_rate') t.depends('sample_rate')
t.translate(lambda k: k['sample_rate']) t.translate(lambda k: k['sample_rate'])
with md.metadata('MDATA_KEY_FTYPE'): with md.metadata('MDATA_KEY_FTYPE') as t:
t.depends('ftype') # i don't think this field even exists t.depends('ftype') # i don't think this field even exists
t.default(u'audioclip') t.default(u'audioclip')
t.translate(lambda k: k['ftype']) # but just in case t.translate(lambda k: k['ftype']) # but just in case
@ -85,9 +95,9 @@ with md.metadata("MDATA_KEY_COPYRIGHT") as t:
t.depends("copyright") t.depends("copyright")
t.max_length(512) t.max_length(512)
with md.metadata("MDATA_KEY_FILEPATH") as t: with md.metadata("MDATA_KEY_ORIGINAL_PATH") as t:
t.depends('path') t.depends('path')
t.translate(lambda k: normpath(k['path'])) t.translate(lambda k: unicode(normpath(k['path'])))
with md.metadata("MDATA_KEY_MD5") as t: with md.metadata("MDATA_KEY_MD5") as t:
t.depends('path') t.depends('path')
@ -96,14 +106,36 @@ with md.metadata("MDATA_KEY_MD5") as t:
# owner is handled differently by (by events.py) # owner is handled differently by (by events.py)
with md.metadata('MDATA_KEY_ORIGINAL_PATH') as t: # MDATA_KEY_TITLE is the annoying special case b/c we sometimes read it
t.depends('original_path') # 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<title>.+)-(\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
# MDATA_KEY_TITLE is the annoying special case
with md.metadata('MDATA_KEY_TITLE') as t: with md.metadata('MDATA_KEY_TITLE') as t:
# Need to know MDATA_KEY_CREATOR to know if show was recorded. Value is # Need to know MDATA_KEY_CREATOR to know if show was recorded. Value is
# defaulted to "" from definitions above # defaulted to "" from definitions above
t.depends('title','MDATA_KEY_CREATOR') t.depends('title','MDATA_KEY_CREATOR','path')
t.optional(False)
t.translate(tr_title)
t.max_length(512) t.max_length(512)
with md.metadata('MDATA_KEY_LABEL') as t: with md.metadata('MDATA_KEY_LABEL') as t:

View File

@ -1,14 +1,36 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from contextlib import contextmanager from contextlib import contextmanager
from media.monitor.pure import truncate_to_length, toposort 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 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): class MetadataAbsent(Exception):
def __init__(self, name): self.name = name def __init__(self, name): self.name = name
def __str__(self): return "Could not obtain element '%s'" % self.name def __str__(self): return "Could not obtain element '%s'" % self.name
class MetadataElement(object): class MetadataElement(Loggable):
def __init__(self,name): def __init__(self,name):
self.name = name self.name = name
# "Sane" defaults # "Sane" defaults
@ -18,6 +40,7 @@ class MetadataElement(object):
self.__default = None self.__default = None
self.__is_normalized = lambda _ : True self.__is_normalized = lambda _ : True
self.__max_length = -1 self.__max_length = -1
self.__translator = None
def max_length(self,l): def max_length(self,l):
self.__max_length = l self.__max_length = l
@ -57,31 +80,64 @@ class MetadataElement(object):
return self.__path return self.__path
def __slice_deps(self, d): 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) return dict( (k,v) for k,v in d.iteritems() if k in self.__deps)
def __str__(self): def __str__(self):
return "%s(%s)" % (self.name, ' '.join(list(self.__deps))) return "%s(%s)" % (self.name, ' '.join(list(self.__deps)))
def read_value(self, path, original, running={}): 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: if self.name in original:
v = original[self.name] v = original[self.name]
if self.__is_normalized(v): return v if self.__is_normalized(v): return v
else: return self.__normalizer(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_orig = self.__slice_deps(original)
dep_slice_running = self.__slice_deps(running) 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() 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 # 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 # If we have a default value then use that. Otherwise throw an
# exception # exception
if self.has_default(): return self.get_default() if self.has_default(): return self.get_default()
else: raise MetadataAbsent(self.name) else: raise MetadataAbsent(self.name)
# We have all dependencies. Now for actual for parsing # 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) ) r = self.__normalizer( self.__translator(full_deps) )
if self.__max_length != -1: if self.__max_length != -1:
r = truncate_to_length(r, self.__max_length) 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 Consumes a path and reads the metadata using mutagen. normalizes some of
the metadata that isn't read through the mutagen hash 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 = {} md = {}
for k,v in m.iteritems(): 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 else: md[k] = v
# populate special metadata values # 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['bitrate'] = getattr(m.info, 'bitrate', u'')
md['sample_rate'] = getattr(m.info, 'sample_rate', 0) md['sample_rate'] = getattr(m.info, 'sample_rate', 0)
md['mime'] = m.mime[0] if len(m.mime) > 0 else u'' 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 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): class MetadataReader(object):
def __init__(self): def __init__(self):
self.clear() self.clear()
def register_metadata(self,m): def register_metadata(self,m):
if m in self.__mdata_name_map:
raise OverwriteMetadataElement(m)
self.__mdata_name_map[m.name] = m self.__mdata_name_map[m.name] = m
d = dict( (name,m.dependencies()) for name,m in d = dict( (name,m.dependencies()) for name,m in
self.__mdata_name_map.iteritems() ) self.__mdata_name_map.iteritems() )
@ -131,6 +203,9 @@ class MetadataReader(object):
if not mdata.is_optional(): raise if not mdata.is_optional(): raise
return normalized_metadata return normalized_metadata
def read_mutagen(self, path):
return self.read(path, normalize_mutagen(path))
global_reader = MetadataReader() global_reader = MetadataReader()
@contextmanager @contextmanager

View File

@ -148,6 +148,9 @@ class BaseEvent(Loggable):
owners.remove_file_owner(self.path) owners.remove_file_owner(self.path)
return ret return ret
except BadSongFile as e: return [e] except BadSongFile as e: return [e]
except Exception as e:
self.unexpected_exception(e)
return[e]
# nothing to see here, please move along # nothing to see here, please move along
def morph_into(self, evt): def morph_into(self, evt):

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pyinotify import pyinotify
from pydispatch import dispatcher from pydispatch import dispatcher
from functools import wraps
import media.monitor.pure as mmp import media.monitor.pure as mmp
from media.monitor.pure import IncludeOnly from media.monitor.pure import IncludeOnly
@ -31,6 +32,7 @@ class FileMediator(object):
def unignore(path): FileMediator.ignored_set.remove(path) def unignore(path): FileMediator.ignored_set.remove(path)
def mediate_ignored(fn): def mediate_ignored(fn):
@wraps(fn)
def wrapped(self, event, *args,**kwargs): def wrapped(self, event, *args,**kwargs):
event.pathname = unicode(event.pathname, "utf-8") event.pathname = unicode(event.pathname, "utf-8")
if FileMediator.is_ignored(event.pathname): if FileMediator.is_ignored(event.pathname):
@ -49,15 +51,11 @@ class OrganizeListener(BaseListener, pyinotify.ProcessEvent, Loggable):
def process_IN_CLOSE_WRITE(self, event): def process_IN_CLOSE_WRITE(self, event):
#self.logger.info("===> handling: '%s'" % str(event)) #self.logger.info("===> handling: '%s'" % str(event))
self.process_to_organize(event) self.process_to_organize(event)
# got cookie
def process_IN_MOVED_TO(self, event): def process_IN_MOVED_TO(self, event):
#self.logger.info("===> handling: '%s'" % str(event)) #self.logger.info("===> handling: '%s'" % str(event))
self.process_to_organize(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): def flush_events(self, path):
""" """
organize the whole directory at path. (pretty much by doing what organize the whole directory at path. (pretty much by doing what
@ -67,6 +65,7 @@ class OrganizeListener(BaseListener, pyinotify.ProcessEvent, Loggable):
for f in mmp.walk_supported(path, clean_empties=True): for f in mmp.walk_supported(path, clean_empties=True):
self.logger.info("Bootstrapping: File in 'organize' directory: \ self.logger.info("Bootstrapping: File in 'organize' directory: \
'%s'" % f) '%s'" % f)
if not mmp.file_locked(f):
dispatcher.send(signal=self.signal, sender=self, dispatcher.send(signal=self.signal, sender=self,
event=OrganizeFile(f)) event=OrganizeFile(f))
flushed += 1 flushed += 1

View File

@ -6,37 +6,30 @@ from media.monitor.pure import LazyProperty
appname = 'root' appname = 'root'
def setup_logging(log_path): 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) #logger = logging.getLogger(appname)
logging.basicConfig(filename=log_path, level=logging.DEBUG) logging.basicConfig(filename=log_path, level=logging.DEBUG)
def get_logger(): def get_logger():
""" """ in case we want to use the common logger from a procedural
in case we want to use the common logger from a procedural interface interface """
"""
return logging.getLogger() return logging.getLogger()
class Loggable(object): class Loggable(object):
""" """ Any class that wants to log can inherit from this class and
Any class that wants to log can inherit from this class and automatically automatically get a logger attribute that can be used like:
get a logger attribute that can be used like: self.logger.info(...) etc. self.logger.info(...) etc. """
"""
__metaclass__ = abc.ABCMeta __metaclass__ = abc.ABCMeta
@LazyProperty @LazyProperty
def logger(self): return get_logger() def logger(self): return get_logger()
def unexpected_exception(self,e): def unexpected_exception(self,e):
""" """ Default message for 'unexpected' exceptions """
Default message for 'unexpected' exceptions
"""
self.fatal_exception("'Unexpected' exception has occured:", e) self.fatal_exception("'Unexpected' exception has occured:", e)
def fatal_exception(self, message, e): def fatal_exception(self, message, e):
""" """ Prints an exception 'e' with 'message'. Also outputs the
Prints an exception 'e' with 'message'. Also outputs the traceback. traceback. """
"""
self.logger.error( message ) self.logger.error( message )
self.logger.error( str(e) ) self.logger.error( str(e) )
self.logger.error( traceback.format_exc() ) self.logger.error( traceback.format_exc() )

View File

@ -18,7 +18,7 @@ class ManagerTimeout(threading.Thread,Loggable):
secnods. This used to be just a work around for cc-4235 but recently secnods. This used to be just a work around for cc-4235 but recently
became a permanent solution because it's "cheap" and reliable 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 # TODO : interval should be read from config and passed here instead
# of just using the hard coded value # of just using the hard coded value
threading.Thread.__init__(self) threading.Thread.__init__(self)
@ -26,7 +26,7 @@ class ManagerTimeout(threading.Thread,Loggable):
self.interval = interval self.interval = interval
def run(self): def run(self):
while True: while True:
time.sleep(self.interval) # every 3 seconds time.sleep(self.interval)
self.manager.flush_organize() self.manager.flush_organize()
class Manager(Loggable): class Manager(Loggable):
@ -178,7 +178,7 @@ class Manager(Loggable):
# the OrganizeListener instance will walk path and dispatch an organize # the OrganizeListener instance will walk path and dispatch an organize
# event for every file in that directory # event for every file in that directory
self.organize['organize_listener'].flush_events(new_path) 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): def flush_organize(self):
path = self.organize['organize_path'] path = self.organize['organize_path']
@ -202,6 +202,12 @@ class Manager(Loggable):
organize. organize.
""" """
store_paths = mmp.expand_storage(store) 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_problem_files_path(store_paths['problem_files'])
self.set_imported_path(store_paths['imported']) self.set_imported_path(store_paths['imported'])
self.set_recorded_path(store_paths['recorded']) self.set_recorded_path(store_paths['recorded'])

View File

@ -2,15 +2,19 @@
import mutagen import mutagen
import os import os
import copy import copy
from collections import namedtuple
from mutagen.easymp4 import EasyMP4KeyError from mutagen.easymp4 import EasyMP4KeyError
from mutagen.easyid3 import EasyID3KeyError from mutagen.easyid3 import EasyID3KeyError
from media.monitor.exceptions import BadSongFile, InvalidMetadataElement from media.monitor.exceptions import BadSongFile, InvalidMetadataElement
from media.monitor.log import Loggable 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 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 list of supported easy tags in mutagen version 1.20
['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', ['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry',
@ -43,21 +47,6 @@ airtime2mutagen = {
"MDATA_KEY_COPYRIGHT" : "copyright", "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 # 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 # 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 # little bit messy. Some of the handling is in m.m.pure while the rest is
# here. Also interface is not very consistent # here. Also interface is not very consistent
# TODO : what is this shit? maybe get rid of it?
@staticmethod @staticmethod
def fix_title(path): def fix_title(path):
# If we have no title in path we will format it # If we have no title in path we will format it
@ -110,39 +100,6 @@ class Metadata(Loggable):
m[u'title'] = new_title m[u'title'] = new_title
m.save() 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 @staticmethod
def write_unsafe(path,md): def write_unsafe(path,md):
""" """
@ -157,6 +114,7 @@ class Metadata(Loggable):
if airtime_k in airtime2mutagen: if airtime_k in airtime2mutagen:
# The unicode cast here is mostly for integers that need to be # The unicode cast here is mostly for integers that need to be
# strings # strings
if airtime_v is None: continue
try: try:
song_file[ airtime2mutagen[airtime_k] ] = unicode(airtime_v) song_file[ airtime2mutagen[airtime_k] ] = unicode(airtime_v)
except (EasyMP4KeyError, EasyID3KeyError) as e: except (EasyMP4KeyError, EasyID3KeyError) as e:
@ -170,44 +128,7 @@ class Metadata(Loggable):
# Forcing the unicode through # Forcing the unicode through
try : fpath = fpath.decode("utf-8") try : fpath = fpath.decode("utf-8")
except : pass except : pass
self.__metadata = global_reader.read_mutagen(fpath)
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)
def is_recorded(self): def is_recorded(self):
""" """

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time
import media.monitor.pure as mmp import media.monitor.pure as mmp
import media.monitor.owners as owners import media.monitor.owners as owners
from media.monitor.handler import ReportHandler from media.monitor.handler import ReportHandler
@ -11,14 +10,12 @@ from os.path import dirname
import os.path import os.path
class Organizer(ReportHandler,Loggable): class Organizer(ReportHandler,Loggable):
""" """ Organizer is responsible to to listening to OrganizeListener
Organizer is responsible to to listening to OrganizeListener events events and committing the appropriate changes to the filesystem.
and committing the appropriate changes to the filesystem. It does It does not in any interact with WatchSyncer's even when the the
not in any interact with WatchSyncer's even when the the WatchSyncer WatchSyncer is a "storage directory". The "storage" directory picks
is a "storage directory". The "storage" directory picks up all of up all of its events through pyinotify. (These events are fed to it
its events through pyinotify. (These events are fed to it through through StoreWatchListener) """
StoreWatchListener)
"""
# Commented out making this class a singleton because it's just a band aid # 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 # 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) super(Organizer, self).__init__(signal=self.channel, weak=False)
def handle(self, sender, event): def handle(self, sender, event):
""" """ Intercept events where a new file has been added to the
Intercept events where a new file has been added to the organize organize directory and place it in the correct path (starting
directory and place it in the correct path (starting with with self.target_path) """
self.target_path)
"""
# Only handle this event type # Only handle this event type
assert isinstance(event, OrganizeFile), \ assert isinstance(event, OrganizeFile), \
"Organizer can only handle OrganizeFile events.Given '%s'" % event "Organizer can only handle OrganizeFile events.Given '%s'" % event
@ -72,13 +67,9 @@ class Organizer(ReportHandler,Loggable):
directory=d) directory=d)
return cb return cb
time.sleep(0.05)
mmp.magic_move(event.path, new_path, mmp.magic_move(event.path, new_path,
after_dir_make=new_dir_watch(dirname(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 # 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 # backwards way is bewcause we are unable to encode the owner id
# into the file itself so that the StoreWatchListener listener can # into the file itself so that the StoreWatchListener listener can

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy import copy
from subprocess import Popen, PIPE
import subprocess import subprocess
import os import os
import math import math
@ -21,7 +22,6 @@ from configobj import ConfigObj
from media.monitor.exceptions import FailedToSetLocale, FailedToCreateDir 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", supported_extensions = [u"mp3", u"ogg", u"oga", u"flac", u"wav",
u'm4a', u'mp4'] u'm4a', u'mp4']
@ -66,7 +66,6 @@ class IncludeOnly(object):
return func(moi, event, *args, **kwargs) return func(moi, event, *args, **kwargs)
return _wrap return _wrap
def partition(f, alist): def partition(f, alist):
""" """
Partition is very similar to filter except that it also returns the 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 # TODO : In the future we would like a better way to find out whether a show
# has been recorded # has been recorded
def is_airtime_recorded(md): def is_airtime_recorded(md):
""" """ Takes a metadata dictionary and returns True if it belongs to a
Takes a metadata dictionary and returns True if it belongs to a file that file that was recorded by Airtime. """
was recorded by Airtime.
"""
if not 'MDATA_KEY_CREATOR' in md: return False if not 'MDATA_KEY_CREATOR' in md: return False
return md['MDATA_KEY_CREATOR'] == u'Airtime Show Recorder' return md['MDATA_KEY_CREATOR'] == u'Airtime Show Recorder'
def read_wave_duration(path): 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: with contextlib.closing(wave.open(path,'r')) as f:
frames = f.getnframes() frames = f.getnframes()
rate = f.getframerate() rate = f.getframerate()
@ -107,9 +105,7 @@ def read_wave_duration(path):
return duration return duration
def clean_empty_dirs(path): 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 # TODO : test this function
if path.endswith('/'): clean_empty_dirs(path[0:-1]) if path.endswith('/'): clean_empty_dirs(path[0:-1])
else: else:
@ -154,22 +150,25 @@ def no_extension_basename(path):
else: return '.'.join(base.split(".")[0:-1]) else: return '.'.join(base.split(".")[0:-1])
def walk_supported(directory, clean_empties=False): def walk_supported(directory, clean_empties=False):
""" """ A small generator wrapper around os.walk to only give us files
A small generator wrapper around os.walk to only give us files that support that support the extensions we are considering. When clean_empties
the extensions we are considering. When clean_empties is True we is True we recursively delete empty directories left over in
recursively delete empty directories left over in directory after the walk. directory after the walk. """
"""
for root, dirs, files in os.walk(directory): for root, dirs, files in os.walk(directory):
full_paths = ( os.path.join(root, name) for name in files full_paths = ( os.path.join(root, name) for name in files
if is_file_supported(name) ) if is_file_supported(name) )
for fp in full_paths: yield fp for fp in full_paths: yield fp
if clean_empties: clean_empty_dirs(directory) 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): def magic_move(old, new, after_dir_make=lambda : None):
""" """ Moves path old to new and constructs the necessary to
Moves path old to new and constructs the necessary to directories for new directories for new along the way """
along the way
"""
new_dir = os.path.dirname(new) new_dir = os.path.dirname(new)
if not os.path.exists(new_dir): os.makedirs(new_dir) if not os.path.exists(new_dir): os.makedirs(new_dir)
# We need this crusty hack because anytime a directory is created we must # 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) shutil.move(old,new)
def move_to_dir(dir_path,file_path): 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) bs = os.path.basename(file_path)
magic_move(file_path, os.path.join(dir_path, bs)) magic_move(file_path, os.path.join(dir_path, bs))
def apply_rules_dict(d, rules): def apply_rules_dict(d, rules):
""" """ Consumes a dictionary of rules that maps some keys to lambdas
Consumes a dictionary of rules that maps some keys to lambdas which it which it applies to every matching element in d and returns a new
applies to every matching element in d and returns a new dictionary with dictionary with the rules applied. If a rule returns none then it's
the rules applied. If a rule returns none then it's not applied not applied """
"""
new_d = copy.deepcopy(d) new_d = copy.deepcopy(d)
for k, rule in rules.iteritems(): for k, rule in rules.iteritems():
if k in d: if k in d:
@ -205,17 +201,14 @@ def default_to_f(dictionary, keys, default, condition):
return new_d return new_d
def default_to(dictionary, keys, default): def default_to(dictionary, keys, default):
""" """ Checks if the list of keys 'keys' exists in 'dictionary'. If
Checks if the list of keys 'keys' exists in 'dictionary'. If not then it not then it returns a new dictionary with all those missing keys
returns a new dictionary with all those missing keys defaults to 'default' defaults to 'default' """
"""
cnd = lambda dictionary, key: key not in dictionary cnd = lambda dictionary, key: key not in dictionary
return default_to_f(dictionary, keys, default, cnd) return default_to_f(dictionary, keys, default, cnd)
def remove_whitespace(dictionary): def remove_whitespace(dictionary):
""" """ Remove values that empty whitespace in the dictionary """
Remove values that empty whitespace in the dictionary
"""
nd = copy.deepcopy(dictionary) nd = copy.deepcopy(dictionary)
bad_keys = [] bad_keys = []
for k,v in nd.iteritems(): for k,v in nd.iteritems():
@ -227,6 +220,7 @@ def remove_whitespace(dictionary):
return nd return nd
def parse_int(s): 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 Tries very hard to get some sort of integer result from s. Defaults to 0
when it fails when it fails
@ -242,53 +236,6 @@ def parse_int(s):
try : return str(reduce(op.add, takewhile(lambda x: x.isdigit(), s))) try : return str(reduce(op.add, takewhile(lambda x: x.isdigit(), s)))
except: return None 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): 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 # TODO : Get rid of this function and every one of its uses. We no longer use
# the md5 signature of a song for anything # the md5 signature of a song for anything
def file_md5(path,max_length=100): def file_md5(path,max_length=100):
""" """ Get md5 of file path (if it exists). Use only max_length
Get md5 of file path (if it exists). Use only max_length characters to save characters to save time and memory. Pass max_length=-1 to read the
time and memory. Pass max_length=-1 to read the whole file (like in mm1) whole file (like in mm1) """
"""
if os.path.exists(path): if os.path.exists(path):
with open(path, 'rb') as f: with open(path, 'rb') as f:
m = hashlib.md5() m = hashlib.md5()
@ -367,16 +313,12 @@ def encode_to(obj, encoding='utf-8'):
return obj return obj
def convert_dict_value_to_utf8(md): 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()]) return dict([(item[0], encode_to(item[1], "utf-8")) for item in md.items()])
def get_system_locale(locale_path='/etc/default/locale'): def get_system_locale(locale_path='/etc/default/locale'):
""" """ Returns the configuration object for the system's default
Returns the configuration object for the system's default locale. Normally locale. Normally requires root access. """
requires root access.
"""
if os.path.exists(locale_path): if os.path.exists(locale_path):
try: try:
config = ConfigObj(locale_path) config = ConfigObj(locale_path)
@ -386,9 +328,7 @@ def get_system_locale(locale_path='/etc/default/locale'):
permissions issue?" % locale_path) permissions issue?" % locale_path)
def configure_locale(config): 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() current_locale = locale.getlocale()
if current_locale[1] is None: if current_locale[1] is None:
default_locale = locale.getdefaultlocale() default_locale = locale.getdefaultlocale()
@ -405,27 +345,21 @@ def configure_locale(config):
def fondle(path,times=None): def fondle(path,times=None):
# TODO : write unit tests for this # TODO : write unit tests for this
""" """ touch a file to change the last modified date. Beware of calling
touch a file to change the last modified date. Beware of calling this this function on the same file from multiple threads. """
function on the same file from multiple threads.
"""
with file(path, 'a'): os.utime(path, times) with file(path, 'a'): os.utime(path, times)
def last_modified(path): def last_modified(path):
""" """ return the time of the last time mm2 was ran. path refers to the
return the time of the last time mm2 was ran. path refers to the index file index file whose date modified attribute contains this information.
whose date modified attribute contains this information. In the case when In the case when the file does not exist we set this time 0 so that
the file does not exist we set this time 0 so that any files on the any files on the filesystem were modified after it """
filesystem were modified after it
"""
if os.path.exists(path): return os.path.getmtime(path) if os.path.exists(path): return os.path.getmtime(path)
else: return 0 else: return 0
def expand_storage(store): def expand_storage(store):
""" """ A storage directory usually consists of 4 different
A storage directory usually consists of 4 different subdirectories. This subdirectories. This function returns their paths """
function returns their paths
"""
store = os.path.normpath(store) store = os.path.normpath(store)
return { return {
'organize' : os.path.join(store, 'organize'), 'organize' : os.path.join(store, 'organize'),
@ -435,10 +369,8 @@ def expand_storage(store):
} }
def create_dir(path): def create_dir(path):
""" """ will try and make sure that path exists at all costs. raises an
will try and make sure that path exists at all costs. raises an exception exception if it fails at this task. """
if it fails at this task.
"""
if not os.path.exists(path): if not os.path.exists(path):
try : os.makedirs(path) try : os.makedirs(path)
except Exception as e : raise FailedToCreateDir(path, e) except Exception as e : raise FailedToCreateDir(path, e)
@ -456,11 +388,10 @@ def sub_path(directory,f):
return common == normalized return common == normalized
def owner_id(original_path): def owner_id(original_path):
""" """ Given 'original_path' return the file name of the of
Given 'original_path' return the file name of the of 'identifier' file. 'identifier' file. return the id that is contained in it. If no file
return the id that is contained in it. If no file is found or nothing is is found or nothing is read then -1 is returned. File is deleted
read then -1 is returned. File is deleted after the number has been read after the number has been read """
"""
fname = "%s.identifier" % original_path fname = "%s.identifier" % original_path
owner_id = -1 owner_id = -1
try: try:
@ -476,9 +407,8 @@ def owner_id(original_path):
return owner_id return owner_id
def file_playable(pathname): def file_playable(pathname):
""" """ Returns True if 'pathname' is playable by liquidsoap. False
Returns True if 'pathname' is playable by liquidsoap. False otherwise. otherwise. """
"""
# when there is an single apostrophe inside of a string quoted by # when there is an single apostrophe inside of a string quoted by
# apostrophes, we can only escape it by replace that apostrophe with # apostrophes, we can only escape it by replace that apostrophe with
# '\''. This breaks the string into two, and inserts an escaped # '\''. 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 assert not data, "A cyclic dependency exists amongst %r" % data
def truncate_to_length(item, length): def truncate_to_length(item, length):
""" """ Truncates 'item' to 'length' """
Truncates 'item' to 'length'
"""
if isinstance(item, int): item = str(item) if isinstance(item, int): item = str(item)
if isinstance(item, basestring): if isinstance(item, basestring):
if len(item) > length: return item[0:length] if len(item) > length: return item[0:length]
else: return item else: return item
def format_length(mutagen_length): def format_length(mutagen_length):
""" """ Convert mutagen length to airtime length """
Convert mutagen length to airtime length
"""
t = float(mutagen_length) t = float(mutagen_length)
h = int(math.floor(t / 3600)) h = int(math.floor(t / 3600))
t = t % 3600 t = t % 3600

View File

@ -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

View File

@ -6,69 +6,9 @@ import copy
from media.monitor.handler import ReportHandler from media.monitor.handler import ReportHandler
from media.monitor.log import Loggable from media.monitor.log import Loggable
from media.monitor.exceptions import BadSongFile from media.monitor.exceptions import BadSongFile
from media.monitor.pure import LazyProperty
from media.monitor.eventcontractor import EventContractor from media.monitor.eventcontractor import EventContractor
from media.monitor.events import EventProxy from media.monitor.events import EventProxy
from media.monitor.request import ThreadedRequestSync, RequestSync
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()
class TimeoutWatcher(threading.Thread,Loggable): class TimeoutWatcher(threading.Thread,Loggable):
""" """
@ -131,8 +71,7 @@ class WatchSyncer(ReportHandler,Loggable):
#self.push_queue( event ) #self.push_queue( event )
except BadSongFile as e: except BadSongFile as e:
self.fatal_exception("Received bas song file '%s'" % e.path, e) self.fatal_exception("Received bas song file '%s'" % e.path, e)
except Exception as e: except Exception as e: self.unexpected_exception(e)
self.unexpected_exception(e)
else: else:
self.logger.info("Received event that does not implement packing.\ self.logger.info("Received event that does not implement packing.\
Printing its representation:") Printing its representation:")
@ -209,8 +148,8 @@ class WatchSyncer(ReportHandler,Loggable):
requests = copy.copy(self.__queue) requests = copy.copy(self.__queue)
def launch_request(): def launch_request():
# Need shallow copy here # Need shallow copy here
t = RequestSync(watcher=self, requests=requests) t = ThreadedRequestSync( RequestSync.create_with_api_client(
t.start() watcher=self, requests=requests) )
self.__current_thread = t self.__current_thread = t
self.__requests.append(launch_request) self.__requests.append(launch_request)
self.__reset_queue() self.__reset_queue()
@ -218,7 +157,8 @@ class WatchSyncer(ReportHandler,Loggable):
def __reset_queue(self): self.__queue = [] def __reset_queue(self): self.__queue = []
def __del__(self): 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(): if self.events_in_queue():
self.logger.warn("Terminating with events still in the queue...") self.logger.warn("Terminating with events still in the queue...")
if self.requests_in_queue(): if self.requests_in_queue():

View File

@ -71,8 +71,10 @@ def get_file_type(file_path):
def calculate_replay_gain(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. This function accepts files of type mp3/ogg/flac and returns a calculated
If the value cannot be calculated for some reason, then we default to 0 (Unity Gain). 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 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:
if file_type == 'mp3': if file_type == 'mp3':
if run_process("which mp3gain > /dev/null") == 0: 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)) command = 'nice -n %s mp3gain -q "%s" 2> /dev/null' \
search = re.search(r'Recommended "Track" dB change: (.*)', out) % (nice_level, temp_file_path)
out = get_process_output(command)
search = re.search(r'Recommended "Track" dB change: (.*)', \
out)
else: else:
logger.warn("mp3gain not found") logger.warn("mp3gain not found")
elif file_type == 'vorbis': elif file_type == 'vorbis':
if run_process("which vorbisgain > /dev/null && which ogginfo > /dev/null") == 0: command = "which vorbisgain > /dev/null && which ogginfo > \
run_process('nice -n %s vorbisgain -q -f "%s" 2>/dev/null >/dev/null' % (nice_level,temp_file_path)) /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) out = get_process_output('ogginfo "%s"' % temp_file_path)
search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out) search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else: else:
logger.warn("vorbisgain/ogginfo not found") logger.warn("vorbisgain/ogginfo not found")
elif file_type == 'flac': elif file_type == 'flac':
if run_process("which metaflac > /dev/null") == 0: 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) search = re.search(r'REPLAYGAIN_TRACK_GAIN=(.*) dB', out)
else: logger.warn("metaflac not found") else: logger.warn("metaflac not found")

View File

@ -59,7 +59,8 @@ def main(global_config, api_client_config, log_config,
try: try:
with open(config['index_path'], 'w') as f: f.write(" ") with open(config['index_path'], 'w') as f: f.write(" ")
except Exception as e: 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: else:
log.info("Created index file, reloading configuration:") log.info("Created index file, reloading configuration:")
main( global_config, api_client_config, log_config, 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) airtime_notifier = AirtimeNotifier(config, airtime_receiver)
store = apiclient.setup_media_monitor() store = apiclient.setup_media_monitor()
log.info("Initing with the following airtime response:%s" % str(store))
airtime_receiver.change_storage({ 'directory':store[u'stor'] }) airtime_receiver.change_storage({ 'directory':store[u'stor'] })
for watch_dir in store[u'watched_dirs']: 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) (given from the database)." % watch_dir)
if os.path.exists(watch_dir): if os.path.exists(watch_dir):
airtime_receiver.new_watch({ 'directory':watch_dir }, restart=True) 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' ) bs = Bootstrapper( db=sdb, watch_signal='watch' )

View File

@ -19,8 +19,8 @@ class TestApiClient(unittest.TestCase):
self.apc.register_component("api-client-tester") self.apc.register_component("api-client-tester")
# All of the following requests should error out in some way # All of the following requests should error out in some way
self.bad_requests = [ self.bad_requests = [
{ 'mode' : 'dang it', 'is_record' : 0 }, { 'mode' : 'foo', 'is_record' : 0 },
{ 'mode' : 'damn frank', 'is_record' : 1 }, { 'mode' : 'bar', 'is_record' : 1 },
{ 'no_mode' : 'at_all' }, ] { 'no_mode' : 'at_all' }, ]
def test_bad_requests(self): def test_bad_requests(self):

View File

@ -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()

View File

@ -26,7 +26,6 @@ class TestMetadata(unittest.TestCase):
i += 1 i += 1
print("Sample metadata: '%s'" % md) print("Sample metadata: '%s'" % md)
self.assertTrue( len( md.keys() ) > 0 ) self.assertTrue( len( md.keys() ) > 0 )
self.assertTrue( 'MDATA_KEY_MD5' in md )
utf8 = md_full.utf8() utf8 = md_full.utf8()
for k,v in md.iteritems(): for k,v in md.iteritems():
if hasattr(utf8[k], 'decode'): if hasattr(utf8[k], 'decode'):
@ -42,10 +41,4 @@ class TestMetadata(unittest.TestCase):
x1 = 123456 x1 = 123456
print("Formatting '%s' to '%s'" % (x1, mmm.format_length(x1))) 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() if __name__ == '__main__': unittest.main()

View File

@ -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()

View File

@ -2,7 +2,6 @@
import unittest import unittest
import os import os
import media.monitor.pure as mmp import media.monitor.pure as mmp
from media.monitor.metadata import Metadata
class TestMMP(unittest.TestCase): class TestMMP(unittest.TestCase):
def setUp(self): def setUp(self):
@ -34,68 +33,6 @@ class TestMMP(unittest.TestCase):
sd = mmp.default_to(dictionary=sd, keys=def_keys, default='DEF') sd = mmp.default_to(dictionary=sd, keys=def_keys, default='DEF')
for k in def_keys: self.assertEqual( sd[k], '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): def test_file_md5(self):
p = os.path.realpath(__file__) p = os.path.realpath(__file__)
m1 = mmp.file_md5(p) 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("123asf"), "123" )
self.assertEqual( mmp.parse_int("asdf"), None ) 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): def test_owner_id(self):
start_path = "testing.mp3" start_path = "testing.mp3"
id_path = "testing.mp3.identifier" id_path = "testing.mp3.identifier"

View File

@ -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()

View File

@ -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

View File

@ -4,6 +4,7 @@
# Enable external Musepack decoder. Requires the # Enable external Musepack decoder. Requires the
# mpcdec binary in the path. Does not work on # mpcdec binary in the path. Does not work on
# Win32. # Win32.
# @category Liquidsoap
def enable_external_mpc_decoder() = def enable_external_mpc_decoder() =
# A list of know extensions and content-type for Musepack. # A list of know extensions and content-type for Musepack.
# Values from http://en.wikipedia.org/wiki/Musepack # Values from http://en.wikipedia.org/wiki/Musepack

View File

@ -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

View File

@ -13,18 +13,12 @@ def http_response(~protocol="HTTP/1.1",
~headers=[], ~headers=[],
~data="") = ~data="") =
status = http_codes[string_of(code)] status = http_codes[string_of(code)]
# Set content-length if needed and not set by the # Set content-length and connection: close
# user.
headers = headers =
if data != "" and list.append(headers,
not list.mem_assoc("Content-Length",headers) [("Content-Length", "#{string.length(data)}"),
then ("Connection", "close")])
list.append([("Content-Length",
"#{string.length(data)}")],
headers)
else
headers
end
headers = list.map(fun (x) -> "#{fst(x)}: #{snd(x)}",headers) headers = list.map(fun (x) -> "#{fst(x)}: #{snd(x)}",headers)
headers = string.concat(separator="\r\n",headers) headers = string.concat(separator="\r\n",headers)
# If no headers are provided, we should avoid # If no headers are provided, we should avoid

View File

@ -5,3 +5,4 @@
%include "flows.liq" %include "flows.liq"
%include "http.liq" %include "http.liq"
%include "video_text.liq" %include "video_text.liq"
%include "gstreamer.liq"

View File

@ -21,10 +21,14 @@ end
# @param a Key to look for # @param a Key to look for
# @param l List of pairs (key,value) # @param l List of pairs (key,value)
def list.mem_assoc(a,l) def list.mem_assoc(a,l)
v = list.assoc(a,l) def f(cur, el) =
# We check for existence, since "" may indicate if not cur then
# either a binding (a,"") or no binding.. fst(el) == a
list.mem((a,v),l) else
cur
end
end
list.fold(f, false, l)
end end
# Remove a pair from an associative list # Remove a pair from an associative list
@ -164,8 +168,7 @@ def out(s)
output.prefered(mksafe(s)) output.prefered(mksafe(s))
end end
# Special track insensitive fallback that # Special track insensitive fallback that always skips current song before switching.
# always skip current song before switching.
# @category Source / Track Processing # @category Source / Track Processing
# @param ~input The input source # @param ~input The input source
# @param f The fallback source # @param f The fallback source
@ -212,14 +215,17 @@ end
# Simple crossfade. # Simple crossfade.
# @category Source / Track Processing # @category Source / Track Processing
# @param ~start_next Duration in seconds of the crossed end of track. # @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_in Duration of the fade in for next track.
# @param ~fade_out Duration of the fade out for previous track # @param ~fade_out Duration of the fade out for previous track.
# @param s The source to use # @param ~conservative Always prepare for a premature end-of-track.
def crossfade(~id="",~start_next,~fade_in,~fade_out,s) # @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.in(duration=fade_in,s)
s = fade.out(duration=fade_out,s) s = fade.out(duration=fade_out,s)
fader = fun (a,b) -> add(normalize=false,[b,a]) 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 end
# Append speech-synthesized tracks reading the metadata. # Append speech-synthesized tracks reading the metadata.
@ -242,8 +248,7 @@ def helium(s)
end end
%endif %endif
# Return true if process exited with 0 code. # Return true if process exited with 0 code. Command should return quickly.
# Command should return quickly.
# @category System # @category System
# @param command Command to test # @param command Command to test
def test_process(command) def test_process(command)
@ -277,12 +282,9 @@ def url.split(uri) =
end end
end end
# Register a server/telnet command to # Register a server/telnet command to update a source's metadata. Returns
# update a source's metadata. Returns # a new source, which will receive the updated metadata. The command has
# a new source, which will receive the # the following format: insert key1="val1",key2="val2",...
# updated metadata. It behaves just like
# the pre-1.0 insert_metadata() operator,
# i.e. insert key1="val1",key2="val2",...
# @category Source / Track Processing # @category Source / Track Processing
# @param ~id Force the value of the source ID. # @param ~id Force the value of the source ID.
def server.insert_metadata(~id="",s) = def server.insert_metadata(~id="",s) =
@ -424,15 +426,15 @@ end
# @param ~conservative Always prepare for a premature end-of-track. # @param ~conservative Always prepare for a premature end-of-track.
# @param ~default Transition used when no rule applies \ # @param ~default Transition used when no rule applies \
# (default: sequence). # (default: sequence).
# @param ~high Value, in dB, for loud sound level # @param ~high Value, in dB, for loud sound level.
# @param ~medium Value, in dB, for medium sound level # @param ~medium Value, in dB, for medium sound level.
# @param ~margin Margin to detect sources that have too different \ # @param ~margin Margin to detect sources that have too different \
# sound level for crossing. # sound level for crossing.
# @param s The input source. # @param s The input source.
def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3., def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
~default=(fun (a,b) -> sequence([a, b])), ~default=(fun (a,b) -> sequence([a, b])),
~high=-15., ~medium=-32., ~margin=4., ~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.out = fade.out(type="sin",duration=fade_out)
fade.in = fade.in(type="sin",duration=fade_in) fade.in = fade.in(type="sin",duration=fade_in)
add = fun (a,b) -> add(normalize=false,[b, a]) 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 if request.resolve(playlist) then
playlist = request.filename(playlist) playlist = request.filename(playlist)
files = playlist.parse(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 else
log(label=id,"Couldn't read playlist: request resolution failed.") log(label=id,"Couldn't read playlist: request resolution failed.")
[] []

View File

@ -1,5 +1,6 @@
%ifdef video.add_text.gd %ifdef video.add_text.gd
# Add a scrolling line of text on video frames. # Add a scrolling line of text on video frames.
# @category Source / Video Processing
# @param ~id Force the value of the source ID. # @param ~id Force the value of the source ID.
# @param ~color Text color (in 0xRRGGBB format). # @param ~color Text color (in 0xRRGGBB format).
# @param ~cycle Cycle text. # @param ~cycle Cycle text.
@ -22,6 +23,7 @@ end
%ifdef video.add_text.sdl %ifdef video.add_text.sdl
# Add a scrolling line of text on video frames. # Add a scrolling line of text on video frames.
# @category Source / Video Processing
# @param ~id Force the value of the source ID. # @param ~id Force the value of the source ID.
# @param ~color Text color (in 0xRRGGBB format). # @param ~color Text color (in 0xRRGGBB format).
# @param ~cycle Cycle text. # @param ~cycle Cycle text.

View File

@ -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, 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))) 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(master_dj, fallible=true))
ignore(output.dummy(dj_live, 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)]) 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, 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))) max=40., on_connect=master_dj_connect, on_disconnect=master_dj_disconnect)))
ignore(output.dummy(master_dj, fallible=true)) 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)]) 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 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, 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))) 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)) 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)]) switch(id="live_dj_switch", track_sensitive=false, transitions=[transition, transition], [({!live_dj_enabled},dj_live), ({true}, s)])
else else

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import traceback
""" """
Python part of radio playout (pypo) Python part of radio playout (pypo)
@ -102,6 +103,24 @@ class Notify:
logger.debug('# Calling server to update webstream data #') logger.debug('# Calling server to update webstream data #')
logger.debug('#################################################') logger.debug('#################################################')
response = self.api_client.notify_webstream_data(data, media_id) 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__': if __name__ == '__main__':
@ -112,41 +131,9 @@ if __name__ == '__main__':
print '#########################################' print '#########################################'
# initialize # initialize
if options.error and options.stream_id:
try: try:
n = Notify() n = Notify()
n.notify_liquidsoap_status(options.error, options.stream_id, options.time) n.run_with_options(options)
except Exception, e: except Exception as e:
print e print( traceback.format_exc() )
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

View File

@ -159,6 +159,7 @@ def WatchAddAction(option, opt, value, parser):
path = currentDir+path path = currentDir+path
path = apc.encode_to(path, 'utf-8') path = apc.encode_to(path, 'utf-8')
if(os.path.isdir(path)): if(os.path.isdir(path)):
os.chmod(path, 0765)
res = api_client.add_watched_dir(path) res = api_client.add_watched_dir(path)
if(res is None): if(res is None):
exit("Unable to connect to the server.") exit("Unable to connect to the server.")