Merge branch 'cc-1799-put-airtime-storage-into-a-human-readable-file-naming-convention' into devel

Conflicts:
	python_apps/media-monitor/MediaMonitor.py
This commit is contained in:
Naomi Aro 2011-06-15 11:02:23 +02:00
commit d5a3aaf3d0
12 changed files with 829 additions and 1688 deletions

View File

@ -4,19 +4,31 @@ define('AIRTIME_VERSION', '1.9.0-devel');
define('AIRTIME_COPYRIGHT_DATE', '2010-2011'); define('AIRTIME_COPYRIGHT_DATE', '2010-2011');
define('AIRTIME_REST_VERSION', '1.1'); define('AIRTIME_REST_VERSION', '1.1');
// Metadata Keys // Metadata Keys for files
define('UI_MDATA_KEY_TITLE', 'dc:title'); define('MDATA_KEY_FILEPATH', 'filepath');
define('UI_MDATA_KEY_CREATOR', 'dc:creator'); define('MDATA_KEY_MD5', 'md5');
define('UI_MDATA_KEY_SOURCE', 'dc:source'); define('MDATA_KEY_TITLE', 'track_title');
define('UI_MDATA_KEY_DURATION', 'dcterms:extent'); define('MDATA_KEY_CREATOR', 'artist_name');
define('UI_MDATA_KEY_URL', 'ls:url'); define('MDATA_KEY_SOURCE', 'album_title');
define('UI_MDATA_KEY_FORMAT', 'dc:format'); define('MDATA_KEY_DURATION', 'length');
define('UI_MDATA_KEY_DESCRIPTION', 'dc:description'); define('MDATA_KEY_MIME', 'mime');
define('UI_MDATA_KEY_CHANNELS', 'ls:channels'); define('MDATA_KEY_FTYPE', 'ftype');
define('UI_MDATA_KEY_SAMPLERATE', 'ls:samplerate'); define('MDATA_KEY_URL', 'url');
define('UI_MDATA_KEY_BITRATE', 'ls:bitrate'); define('MDATA_KEY_GENRE', 'genre');
define('UI_MDATA_KEY_ENCODER', 'ls:encoder'); define('MDATA_KEY_MOOD', 'mood');
define('UI_MDATA_KEY_FILENAME', 'ls:filename'); define('MDATA_KEY_LABEL', 'label');
define('MDATA_KEY_COMPOSER', 'composer');
define('MDATA_KEY_FORMAT', 'format');
define('MDATA_KEY_DESCRIPTION', 'description');
define('MDATA_KEY_SAMPLERATE', 'sample_rate');
define('MDATA_KEY_BITRATE', 'bit_rate');
define('MDATA_KEY_ENCODER', 'encoded_by');
define('MDATA_KEY_ISRC', 'isrc_number');
define('MDATA_KEY_COPYRIGHT', 'copyright');
define('MDATA_KEY_YEAR', 'year');
define('MDATA_KEY_BPM', 'bpm');
define('MDATA_KEY_TRACKNUMBER', 'track_number');
define('MDATA_KEY_CONDUCTOR', 'conductor');
define('UI_MDATA_VALUE_FORMAT_FILE', 'File'); define('UI_MDATA_VALUE_FORMAT_FILE', 'File');
define('UI_MDATA_VALUE_FORMAT_STREAM', 'live stream'); define('UI_MDATA_VALUE_FORMAT_STREAM', 'live stream');

View File

@ -10,6 +10,8 @@ class ApiController extends Zend_Controller_Action
$context->addActionContext('version', 'json') $context->addActionContext('version', 'json')
->addActionContext('recorded-shows', 'json') ->addActionContext('recorded-shows', 'json')
->addActionContext('upload-recorded', 'json') ->addActionContext('upload-recorded', 'json')
->addActionContext('media-monitor-setup', 'json')
->addActionContext('media-item-status', 'json')
->addActionContext('reload-metadata', 'json') ->addActionContext('reload-metadata', 'json')
->initContext(); ->initContext();
} }
@ -77,7 +79,7 @@ class ApiController extends Zend_Controller_Action
if (ctype_alnum($file_id) && strlen($file_id) == 32) { if (ctype_alnum($file_id) && strlen($file_id) == 32) {
$media = StoredFile::RecallByGunid($file_id); $media = StoredFile::RecallByGunid($file_id);
if ($media != null && !PEAR::isError($media)) { if ($media != null && !PEAR::isError($media)) {
$filepath = $media->getRealFilePath(); $filepath = $media->getFilePath();
if(!is_file($filepath)) if(!is_file($filepath))
{ {
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
@ -293,17 +295,17 @@ class ApiController extends Zend_Controller_Action
print 'You are not allowed to access this resource.'; print 'You are not allowed to access this resource.';
exit; exit;
} }
$showCanceled = false; $showCanceled = false;
$show_instance = $this->_getParam('show_instance'); $show_instance = $this->_getParam('show_instance');
$upload_dir = ini_get("upload_tmp_dir"); $upload_dir = ini_get("upload_tmp_dir");
$file = StoredFile::uploadFile($upload_dir); $file = StoredFile::uploadFile($upload_dir);
$show_name = ""; $show_name = "";
try { try {
$show_inst = new ShowInstance($show_instance); $show_inst = new ShowInstance($show_instance);
$show_inst->setRecordedFile($file->getId()); $show_inst->setRecordedFile($file->getId());
$show_name = $show_inst->getName(); $show_name = $show_inst->getName();
$show_genre = $show_inst->getGenre(); $show_genre = $show_inst->getGenre();
@ -317,12 +319,12 @@ class ApiController extends Zend_Controller_Action
//the library), now lets just return. //the library), now lets just return.
$showCanceled = true; $showCanceled = true;
} }
$tmpTitle = !(empty($show_name))?$show_name."-":""; $tmpTitle = !(empty($show_name))?$show_name."-":"";
$tmpTitle .= $file->getName(); $tmpTitle .= $file->getName();
$file->setMetadataValue(UI_MDATA_KEY_TITLE, $tmpTitle); $file->setMetadataValue(UI_MDATA_KEY_TITLE, $tmpTitle);
if (!$showCanceled && Application_Model_Preference::GetDoSoundCloudUpload()) if (!$showCanceled && Application_Model_Preference::GetDoSoundCloudUpload())
{ {
for ($i=0; $i<$CC_CONFIG['soundcloud-connection-retries']; $i++) { for ($i=0; $i<$CC_CONFIG['soundcloud-connection-retries']; $i++) {
@ -353,8 +355,57 @@ class ApiController extends Zend_Controller_Action
$this->view->id = $file->getId(); $this->view->id = $file->getId();
} }
public function reloadMetadataAction() { public function mediaMonitorSetupAction() {
global $CC_CONFIG;
// disable the view and the layout
$this->view->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
$api_key = $this->_getParam('api_key');
if (!in_array($api_key, $CC_CONFIG["apiKey"]))
{
header('HTTP/1.0 401 Unauthorized');
print 'You are not allowed to access this resource.';
exit;
}
$plupload_dir = ini_get("upload_tmp_dir") . DIRECTORY_SEPARATOR . "plupload";
//need to make sure plupload dir exists so we can watch it.
if(!file_exists($plupload_dir)) {
@mkdir($plupload_dir, 0755);
}
$this->view->stor = $CC_CONFIG['storageDir'];
$this->view->plupload = $plupload_dir;
}
public function mediaItemStatusAction() {
global $CC_CONFIG;
$api_key = $this->_getParam('api_key');
if (!in_array($api_key, $CC_CONFIG["apiKey"]))
{
header('HTTP/1.0 401 Unauthorized');
print 'You are not allowed to access this resource.';
exit;
}
$md5 = $this->_getParam('md5');
$file = StoredFile::RecallByMd5($md5);
//New file added to Airtime
if (is_null($file)) {
$this->view->airtime_status = 0;
}
else {
$this->view->airtime_status = 1;
}
}
public function reloadMetadataAction() {
global $CC_CONFIG; global $CC_CONFIG;
$api_key = $this->_getParam('api_key'); $api_key = $this->_getParam('api_key');
@ -366,22 +417,62 @@ class ApiController extends Zend_Controller_Action
} }
$md = $this->_getParam('md'); $md = $this->_getParam('md');
$filepath = $md['filepath']; $mode = $this->_getParam('mode');
$filepath = str_replace("\\", "", $filepath);
$file = StoredFile::Recall(null, null, null, $filepath); if ($mode == "create") {
if (PEAR::isError($file) || is_null($file)) { $md5 = $md['MDATA_KEY_MD5'];
$this->view->response = "File not in Airtime's Database"; $file = StoredFile::RecallByMd5($md5);
return;
if (is_null($file)) {
$file = StoredFile::Insert($md);
}
else {
$this->view->error = "File already exists in Airtime.";
return;
}
}
else if ($mode == "modify") {
$filepath = $md['MDATA_KEY_FILEPATH'];
$filepath = str_replace("\\", "", $filepath);
$file = StoredFile::RecallByFilepath($filepath);
//File is not in database anymore.
if (is_null($file)) {
$this->view->error = "File does not exist in Airtime.";
return;
}
//Updating a metadata change.
else {
$file->setMetadata($md);
}
}
else if ($mode == "moved") {
$md5 = $md['MDATA_KEY_MD5'];
$file = StoredFile::RecallByMd5($md5);
if (is_null($file)) {
$this->view->error = "File doesn't exist in Airtime.";
return;
}
else {
$file->setMetadata($md);
}
}
else if ($mode == "delete") {
$filepath = $md['MDATA_KEY_FILEPATH'];
$filepath = str_replace("\\", "", $filepath);
$file = StoredFile::RecallByFilepath($filepath);
if (is_null($file)) {
$this->view->error = "File doesn't exist in Airtime.";
return;
}
else {
$file->delete();
}
} }
$res = $file->replaceDbMetadata($md); $this->view->id = $file->getId();
if (PEAR::isError($res)) {
$this->view->response = "Metadata Change Failed";
}
else {
$this->view->response = "Success!";
}
} }
} }

View File

@ -78,7 +78,7 @@ class LibraryController extends Zend_Controller_Action
$file_id = $this->_getParam('id', null); $file_id = $this->_getParam('id', null);
$file = StoredFile::Recall($file_id); $file = StoredFile::Recall($file_id);
$url = $file->getFileURL().'/api_key/'.$CC_CONFIG["apiKey"][0].'/download/true'; $url = $file->getFileUrl().'/api_key/'.$CC_CONFIG["apiKey"][0].'/download/true';
$menu[] = array('action' => array('type' => 'gourl', 'url' => $url), $menu[] = array('action' => array('type' => 'gourl', 'url' => $url),
'title' => 'Download'); 'title' => 'Download');
@ -162,18 +162,18 @@ class LibraryController extends Zend_Controller_Action
if ($form->isValid($request->getPost())) { if ($form->isValid($request->getPost())) {
$formdata = $form->getValues(); $formdata = $form->getValues();
$file->replaceDbMetadata($formdata); $file->setDbColMetadata($formdata);
$data = $formdata; $data = $formdata;
$data['filepath'] = $file->getRealFilePath(); $data['filepath'] = $file->getFilePath();
//wait for 1.9.0 release
//RabbitMq::SendFileMetaData($data); RabbitMq::SendFileMetaData($data);
$this->_helper->redirector('index'); $this->_helper->redirector('index');
} }
} }
$form->populate($file->md); $form->populate($file->getDbColMetadata());
$this->view->form = $form; $this->view->form = $form;
} }

View File

@ -25,9 +25,13 @@ class PluploadController extends Zend_Controller_Action
public function uploadAction() public function uploadAction()
{ {
$upload_dir = ini_get("upload_tmp_dir") . DIRECTORY_SEPARATOR . "plupload"; $upload_dir = ini_get("upload_tmp_dir") . DIRECTORY_SEPARATOR . "plupload";
$file = StoredFile::uploadFile($upload_dir); $res = StoredFile::uploadFile($upload_dir);
die('{"jsonrpc" : "2.0", "id" : '.$file->getId().' }'); if (isset($res)) {
die('{"jsonrpc" : "2.0", "id" : '.$file->getMessage().' }');
}
die('{"jsonrpc" : "2.0"}');
} }
} }

View File

@ -40,9 +40,6 @@ class RabbitMq
} }
} }
/*
* wait for 1.9.0 release
public static function SendFileMetaData($md) public static function SendFileMetaData($md)
{ {
global $CC_CONFIG; global $CC_CONFIG;
@ -64,7 +61,5 @@ class RabbitMq
$channel->close(); $channel->close();
$conn->close(); $conn->close();
} }
*/
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,11 +11,11 @@ SCRIPTPATH=`dirname $SCRIPT`
echo -e "\n******************************** Install Begin *********************************" echo -e "\n******************************** Install Begin *********************************"
php ${SCRIPTPATH}/airtime-install.php $@
echo -e "\n*** Creating Pypo User ***" echo -e "\n*** Creating Pypo User ***"
python ${SCRIPTPATH}/../python_apps/create-pypo-user.py python ${SCRIPTPATH}/../python_apps/create-pypo-user.py
php ${SCRIPTPATH}/airtime-install.php $@
echo -e "\n*** Pypo Installation ***" echo -e "\n*** Pypo Installation ***"
python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-install.py python ${SCRIPTPATH}/../python_apps/pypo/install/pypo-install.py

View File

@ -121,8 +121,6 @@ if ($db_install) {
AirtimeInstall::InstallStorageDirectory(); AirtimeInstall::InstallStorageDirectory();
AirtimeInstall::ChangeDirOwnerToWebserver($CC_CONFIG["storageDir"]);
AirtimeInstall::CreateSymlinksToUtils(); AirtimeInstall::CreateSymlinksToUtils();
AirtimeInstall::CreateZendPhpLogFile(); AirtimeInstall::CreateZendPhpLogFile();

View File

@ -116,42 +116,39 @@ class AirtimeInstall
} }
} }
public static function ChangeDirOwnerToWebserver($filePath)
{
global $CC_CONFIG;
echo "* Giving Apache permission to access $filePath".PHP_EOL;
$success = chgrp($filePath, $CC_CONFIG["webServerUser"]);
$fileperms=@fileperms($filePath);
$fileperms = $fileperms | 0x0010; // group write bit
$fileperms = $fileperms | 0x0400; // group sticky bit
chmod($filePath, $fileperms);
}
public static function InstallStorageDirectory() public static function InstallStorageDirectory()
{ {
global $CC_CONFIG, $CC_DBC; global $CC_CONFIG, $CC_DBC;
echo "* Storage directory setup".PHP_EOL; echo "* Storage directory setup".PHP_EOL;
foreach (array('baseFilesDir', 'storageDir') as $d) { $stor_dir = $CC_CONFIG['storageDir'];
if ( !file_exists($CC_CONFIG[$d]) ) {
@mkdir($CC_CONFIG[$d], 02775, true); if (!file_exists($stor_dir)) {
if (file_exists($CC_CONFIG[$d])) { @mkdir($stor_dir, 02777, true);
$rp = realpath($CC_CONFIG[$d]); if (file_exists($stor_dir)) {
echo "* Directory $rp created".PHP_EOL; $rp = realpath($stor_dir);
} else { echo "* Directory $rp created".PHP_EOL;
echo "* Failed creating {$CC_CONFIG[$d]}".PHP_EOL;
exit(1);
}
} elseif (is_writable($CC_CONFIG[$d])) {
$rp = realpath($CC_CONFIG[$d]);
echo "* Skipping directory already exists: $rp".PHP_EOL;
} else { } else {
$rp = realpath($CC_CONFIG[$d]); echo "* Failed creating {$stor_dir}".PHP_EOL;
echo "* WARNING: Directory already exists, but is not writable: $rp".PHP_EOL; exit(1);
} }
$CC_CONFIG[$d] = $rp;
} }
else if (is_writable($stor_dir)) {
$rp = realpath($stor_dir);
echo "* Skipping directory already exists: $rp".PHP_EOL;
}
else {
$rp = realpath($stor_dir);
echo "* WARNING: Directory already exists, but is not writable: $rp".PHP_EOL;
return;
}
echo "* Giving Apache permission to access $rp".PHP_EOL;
$success = chgrp($rp, $CC_CONFIG["webServerUser"]);
$success = chown($rp, "pypo");
$success = chmod($rp, 02777);
$CC_CONFIG['storageDir'] = $rp;
} }
public static function CreateDatabaseUser() public static function CreateDatabaseUser()
@ -300,7 +297,7 @@ class AirtimeInstall
echo "* Installing airtime-update-db-settings".PHP_EOL; echo "* Installing airtime-update-db-settings".PHP_EOL;
$dir = AirtimeInstall::CONF_DIR_BINARIES."/utils/airtime-update-db-settings"; $dir = AirtimeInstall::CONF_DIR_BINARIES."/utils/airtime-update-db-settings";
exec("ln -s $dir /usr/bin/airtime-update-db-settings"); exec("ln -s $dir /usr/bin/airtime-update-db-settings");
echo "* Installing airtime-check-system".PHP_EOL; echo "* Installing airtime-check-system".PHP_EOL;
$dir = AirtimeInstall::CONF_DIR_BINARIES."/utils/airtime-check-system"; $dir = AirtimeInstall::CONF_DIR_BINARIES."/utils/airtime-check-system";
exec("ln -s $dir /usr/bin/airtime-check-system"); exec("ln -s $dir /usr/bin/airtime-check-system");

View File

@ -117,6 +117,9 @@ class ApiClientInterface:
def upload_recorded_show(self): def upload_recorded_show(self):
pass pass
def check_media_status(self, md5):
pass
def update_media_metadata(self, md): def update_media_metadata(self, md):
pass pass
@ -356,13 +359,52 @@ class AirTimeApiClient(ApiClientInterface):
return response return response
def update_media_metadata(self, md): def setup_media_monitor(self):
logger = logging.getLogger()
response = None
try:
url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["media_setup_url"])
url = url.replace("%%api_key%%", self.config["api_key"])
logger.debug(url)
response = urllib.urlopen(url)
response = json.loads(response.read())
logger.debug("Json Media Setup %s", response)
except Exception, e:
response = None
logger.error("Exception: %s", e)
return response
def check_media_status(self, md5):
logger = logging.getLogger()
response = None
try:
url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["media_status_url"])
url = url.replace("%%api_key%%", self.config["api_key"])
url = url.replace("%%md5%%", md5)
logger.debug(url)
response = urllib.urlopen(url)
response = json.loads(response.read())
logger.info("Json Media Status %s", response)
except Exception, e:
logger.error("Exception: %s", e)
return response
def update_media_metadata(self, md, mode):
logger = logging.getLogger() logger = logging.getLogger()
response = None response = None
try: try:
url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_media_url"]) url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_media_url"])
logger.debug(url) logger.debug(url)
url = url.replace("%%api_key%%", self.config["api_key"]) url = url.replace("%%api_key%%", self.config["api_key"])
url = url.replace("%%mode%%", mode)
data = recursive_urlencode(md) data = recursive_urlencode(md)
req = urllib2.Request(url, data) req = urllib2.Request(url, data)
@ -372,6 +414,7 @@ class AirTimeApiClient(ApiClientInterface):
response = json.loads(response) response = json.loads(response)
except Exception, e: except Exception, e:
response = None
logger.error("Exception: %s", e) logger.error("Exception: %s", e)
return response return response

View File

@ -9,7 +9,10 @@ import sys
import hashlib import hashlib
import json import json
import shutil import shutil
import math
from collections import deque
from pwd import getpwnam
from subprocess import Popen, PIPE, STDOUT from subprocess import Popen, PIPE, STDOUT
from configobj import ConfigObj from configobj import ConfigObj
@ -23,8 +26,13 @@ from kombu.connection import BrokerConnection
from kombu.messaging import Exchange, Queue, Consumer, Producer from kombu.messaging import Exchange, Queue, Consumer, Producer
from api_clients import api_client from api_clients import api_client
MODE_CREATE = "create"
MODE_MODIFY = "modify"
MODE_MOVED = "moved"
MODE_DELETE = "delete"
global storage_directory global storage_directory
storage_directory = "/srv/airtime/stor" global plupload_directory
# configure logging # configure logging
try: try:
@ -46,35 +54,27 @@ list of supported easy tags in mutagen version 1.20
['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', 'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby', 'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid', 'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort', 'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor', 'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid', 'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid', 'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid', 'arranger', 'albumsort', 'replaygain_*_peak', 'organization'] ['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry', 'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby', 'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid', 'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort', 'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor', 'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid', 'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid', 'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm', 'musicbrainz_trackid', 'arranger', 'albumsort', 'replaygain_*_peak', 'organization']
""" """
def checkRabbitMQ(notifier):
try:
notifier.connection.drain_events(timeout=int(config["check_airtime_events"]))
except Exception, e:
logger = logging.getLogger('root')
logger.info("%s", e)
class AirtimeNotifier(Notifier): class AirtimeNotifier(Notifier):
def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, threshold=0, timeout=None): def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, threshold=0, timeout=None):
Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, threshold, timeout) Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, threshold, timeout)
self.airtime2mutagen = {\ self.airtime2mutagen = {\
"track_title": "title",\ "MDATA_KEY_TITLE": "title",\
"artist_name": "artist",\ "MDATA_KEY_CREATOR": "artist",\
"album_title": "album",\ "MDATA_KEY_SOURCE": "album",\
"genre": "genre",\ "MDATA_KEY_GENRE": "genre",\
"mood": "mood",\ "MDATA_KEY_MOOD": "mood",\
"track_number": "tracknumber",\ "MDATA_KEY_TRACKNUMBER": "tracknumber",\
"bpm": "bpm",\ "MDATA_KEY_BPM": "bpm",\
"label": "organization",\ "MDATA_KEY_LABEL": "organization",\
"composer": "composer",\ "MDATA_KEY_COMPOSER": "composer",\
"encoded_by": "encodedby",\ "MDATA_KEY_ENCODER": "encodedby",\
"conductor": "conductor",\ "MDATA_KEY_CONDUCTOR": "conductor",\
"year": "date",\ "MDATA_KEY_YEAR": "date",\
"info_url": "website",\ "MDATA_KEY_URL": "website",\
"isrc_number": "isrc",\ "MDATA_KEY_ISRC": "isrc",\
"copyright": "copyright",\ "MDATA_KEY_COPYRIGHT": "copyright",\
} }
schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True) schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True)
@ -112,27 +112,48 @@ class MediaMonitor(ProcessEvent):
self.api_client = api_client.api_client_factory(config) self.api_client = api_client.api_client_factory(config)
self.mutagen2airtime = {\ self.mutagen2airtime = {\
"title": "track_title",\ "title": "MDATA_KEY_TITLE",\
"artist": "artist_name",\ "artist": "MDATA_KEY_CREATOR",\
"album": "album_title",\ "album": "MDATA_KEY_SOURCE",\
"genre": "genre",\ "genre": "MDATA_KEY_GENRE",\
"mood": "mood",\ "mood": "MDATA_KEY_MOOD",\
"tracknumber": "track_number",\ "tracknumber": "MDATA_KEY_TRACKNUMBER",\
"bpm": "bpm",\ "bpm": "MDATA_KEY_BPM",\
"organization": "label",\ "organization": "MDATA_KEY_LABEL",\
"composer": "composer",\ "composer": "MDATA_KEY_COMPOSER",\
"encodedby": "encoded_by",\ "encodedby": "MDATA_KEY_ENCODER",\
"conductor": "conductor",\ "conductor": "MDATA_KEY_CONDUCTOR",\
"date": "year",\ "date": "MDATA_KEY_YEAR",\
"website": "info_url",\ "website": "MDATA_KEY_URL",\
"isrc": "isrc_number",\ "isrc": "MDATA_KEY_ISRC",\
"copyright": "copyright",\ "copyright": "MDATA_KEY_COPYRIGHT",\
} }
self.supported_file_formats = ['mp3', 'ogg'] self.supported_file_formats = ['mp3', 'ogg']
self.logger = logging.getLogger('root') self.logger = logging.getLogger('root')
self.temp_files = {} self.temp_files = {}
self.imported_renamed_files = {} self.moved_files = {}
self.file_events = deque()
self.mask = pyinotify.IN_CREATE | \
pyinotify.IN_MODIFY | \
pyinotify.IN_MOVED_FROM | \
pyinotify.IN_MOVED_TO | \
pyinotify.IN_DELETE | \
pyinotify.IN_DELETE_SELF
self.wm = WatchManager()
schedule_exchange = Exchange("airtime-media-monitor", "direct", durable=True, auto_delete=True)
schedule_queue = Queue("media-monitor", exchange=schedule_exchange, key="filesystem")
connection = BrokerConnection(config["rabbitmq_host"], config["rabbitmq_user"], config["rabbitmq_password"], "/")
channel = connection.channel()
def watch_directory(self, directory):
return self.wm.add_watch(directory, self.mask, rec=True, auto_add=True)
def is_parent_directory(self, filepath, directory):
return (directory == filepath[0:len(directory)])
def get_md5(self, filepath): def get_md5(self, filepath):
f = open(filepath, 'rb') f = open(filepath, 'rb')
@ -142,20 +163,40 @@ class MediaMonitor(ProcessEvent):
return md5 return md5
def ensure_dir(self, filepath): ## mutagen_length is in seconds with the format (d+).dd
## return format hh:mm:ss.uuu
def format_length(self, mutagen_length):
t = float(mutagen_length)
h = int(math.floor(t/3600))
t = t % 3600
m = int(math.floor(t/60))
s = t % 60
# will be ss.uuu
s = str(s)
s = s[:6]
length = "%s:%s:%s" % (h, m, s)
return length
def ensure_dir(self, filepath):
directory = os.path.dirname(filepath) directory = os.path.dirname(filepath)
if ((not os.path.exists(directory)) or ((os.path.exists(directory) and not os.path.isdir(directory)))): try:
os.makedirs(directory, 02775) omask = os.umask(0)
if ((not os.path.exists(directory)) or ((os.path.exists(directory) and not os.path.isdir(directory)))):
os.makedirs(directory, 02777)
self.watch_directory(directory)
finally:
os.umask(omask)
def create_unique_filename(self, filepath): def create_unique_filename(self, filepath):
file_dir = os.path.dirname(filepath)
filename = os.path.basename(filepath).split(".")[0]
file_ext = os.path.splitext(filepath)[1]
if(os.path.exists(filepath)): if(os.path.exists(filepath)):
file_dir = os.path.dirname(filepath)
filename = os.path.basename(filepath).split(".")[0]
file_ext = os.path.splitext(filepath)[1]
i = 1; i = 1;
while(True): while(True):
new_filepath = "%s/%s(%s).%s" % (file_dir, filename, i, file_ext) new_filepath = "%s/%s(%s).%s" % (file_dir, filename, i, file_ext)
@ -165,79 +206,119 @@ class MediaMonitor(ProcessEvent):
else: else:
filepath = new_filepath filepath = new_filepath
self.imported_renamed_files[filepath] = 0
return filepath return filepath
def create_file_path(self, imported_filepath): def create_file_path(self, imported_filepath):
global storage_directory global storage_directory
original_name = os.path.basename(imported_filepath)
file_ext = os.path.splitext(imported_filepath)[1]
file_info = mutagen.File(imported_filepath, easy=True)
metadata = {'artist':None,
'album':None,
'title':None,
'tracknumber':None}
for key in metadata.keys():
if key in file_info:
metadata[key] = file_info[key][0]
if metadata['artist'] is not None:
base = "%s/%s" % (storage_directory, metadata['artist'])
if metadata['album'] is not None:
base = "%s/%s" % (base, metadata['album'])
if metadata['title'] is not None:
if metadata['tracknumber'] is not None:
metadata['tracknumber'] = "%02d" % (int(metadata['tracknumber']))
base = "%s/%s - %s" % (base, metadata['tracknumber'], metadata['title'])
else:
base = "%s/%s" % (base, metadata['title'])
else:
base = "%s/%s" % (base, original_name)
else:
base = "%s/%s" % (storage_directory, original_name)
base = "%s%s" % (base, file_ext)
filepath = self.create_unique_filename(base)
self.ensure_dir(filepath)
shutil.move(imported_filepath, filepath)
def update_airtime(self, event):
self.logger.info("Updating Change to Airtime")
try: try:
md5 = self.get_md5(event.pathname) #get rid of file extention from original name, name might have more than 1 '.' in it.
md = {'filepath':event.pathname, 'md5':md5} original_name = os.path.basename(imported_filepath)
#self.logger.info('original name: %s', original_name)
original_name = original_name.split(".")[0:-1]
#self.logger.info('original name: %s', original_name)
original_name = ''.join(original_name)
#self.logger.info('original name: %s', original_name)
file_info = mutagen.File(event.pathname, easy=True) file_ext = os.path.splitext(imported_filepath)[1]
attrs = self.mutagen2airtime file_info = mutagen.File(imported_filepath, easy=True)
for key in file_info.keys() :
if key in attrs :
md[attrs[key]] = file_info[key][0]
data = {'md': md} metadata = {'artist':None,
response = self.api_client.update_media_metadata(data) 'album':None,
'title':None,
'tracknumber':None}
for key in metadata.keys():
if key in file_info:
metadata[key] = file_info[key][0]
if metadata['artist'] is not None:
base = "%s/%s" % (storage_directory, metadata['artist'])
if metadata['album'] is not None:
base = "%s/%s" % (base, metadata['album'])
if metadata['title'] is not None:
if metadata['tracknumber'] is not None:
metadata['tracknumber'] = "%02d" % (int(metadata['tracknumber']))
base = "%s/%s - %s" % (base, metadata['tracknumber'], metadata['title'])
else:
base = "%s/%s" % (base, metadata['title'])
else:
base = "%s/%s" % (base, original_name)
else:
base = "%s/%s" % (storage_directory, original_name)
base = "%s%s" % (base, file_ext)
filepath = self.create_unique_filename(base)
self.ensure_dir(filepath)
except Exception, e: except Exception, e:
self.logger.info("%s", e) self.logger.error('Exception: %s', e)
def is_renamed_file(self, filename): return filepath
if filename in self.imported_renamed_files:
del self.imported_renamed_files[filename]
return True
return False def get_mutagen_info(self, filepath):
md = {}
md5 = self.get_md5(filepath)
md['MDATA_KEY_MD5'] = md5
file_info = mutagen.File(filepath, easy=True)
attrs = self.mutagen2airtime
for key in file_info.keys() :
if key in attrs :
md[attrs[key]] = file_info[key][0]
md['MDATA_KEY_BITRATE'] = file_info.info.bitrate
md['MDATA_KEY_SAMPLERATE'] = file_info.info.sample_rate
md['MDATA_KEY_DURATION'] = self.format_length(file_info.info.length)
md['MDATA_KEY_MIME'] = file_info.mime[0]
if "mp3" in md['MDATA_KEY_MIME']:
md['MDATA_KEY_FTYPE'] = "audioclip"
elif "vorbis" in md['MDATA_KEY_MIME']:
md['MDATA_KEY_FTYPE'] = "audioclip"
return md
def update_airtime(self, d):
filepath = d['filepath']
mode = d['mode']
data = None
md = {}
md['MDATA_KEY_FILEPATH'] = filepath
if (os.path.exists(filepath) and (mode == MODE_CREATE)):
mutagen = self.get_mutagen_info(filepath)
md.update(mutagen)
data = {'md': md}
elif (os.path.exists(filepath) and (mode == MODE_MODIFY)):
mutagen = self.get_mutagen_info(filepath)
md.update(mutagen)
data = {'md': md}
elif (mode == MODE_MOVED):
mutagen = self.get_mutagen_info(filepath)
md.update(mutagen)
data = {'md': md}
elif (mode == MODE_DELETE):
data = {'md': md}
if data is not None:
self.logger.info("Updating Change to Airtime")
response = None
while response is None:
response = self.api_client.update_media_metadata(data, mode)
time.sleep(5)
def is_temp_file(self, filename): def is_temp_file(self, filename):
info = filename.split(".") info = filename.split(".")
if(info[-2] in self.supported_file_formats): if(info[-2] in self.supported_file_formats):
return True return True
else : else:
return False return False
def is_audio_file(self, filename): def is_audio_file(self, filename):
@ -245,63 +326,124 @@ class MediaMonitor(ProcessEvent):
if(info[-1] in self.supported_file_formats): if(info[-1] in self.supported_file_formats):
return True return True
else : else:
return False return False
def process_IN_CREATE(self, event): def process_IN_CREATE(self, event):
if not event.dir: if not event.dir:
self.logger.info("%s: %s", event.maskname, event.pathname)
#file created is a tmp file which will be modified and then moved back to the original filename. #file created is a tmp file which will be modified and then moved back to the original filename.
if self.is_temp_file(event.name) : if self.is_temp_file(event.name) :
self.temp_files[event.pathname] = None self.temp_files[event.pathname] = None
#This is a newly imported file. #This is a newly imported file.
else : else :
#if not is_renamed_file(event.pathname): global plupload_directory
self.create_file_path(event.pathname) #files that have been added through plupload have a placeholder already put in Airtime's database.
if not self.is_parent_directory(event.pathname, plupload_directory):
md5 = self.get_md5(event.pathname)
response = self.api_client.check_media_status(md5)
self.logger.info("%s: %s", event.maskname, event.pathname) #this file is new, md5 does not exist in Airtime.
if(response['airtime_status'] == 0):
filepath = self.create_file_path(event.pathname)
os.rename(event.pathname, filepath)
self.file_events.append({'mode': MODE_CREATE, 'filepath': filepath})
def process_IN_MODIFY(self, event): def process_IN_MODIFY(self, event):
if not event.dir : if not event.dir:
self.logger.info("%s: %s", event.maskname, event.pathname)
if self.is_audio_file(event.name) : global plupload_directory
self.update_airtime(event) #files that have been added through plupload have a placeholder already put in Airtime's database.
if not self.is_parent_directory(event.pathname, plupload_directory):
self.logger.info("%s: %s", event.maskname, event.pathname) if self.is_audio_file(event.name) :
self.file_events.append({'filepath': event.pathname, 'mode': MODE_MODIFY})
def process_IN_MOVED_FROM(self, event): def process_IN_MOVED_FROM(self, event):
if event.pathname in self.temp_files : self.logger.info("%s: %s", event.maskname, event.pathname)
if event.pathname in self.temp_files:
del self.temp_files[event.pathname] del self.temp_files[event.pathname]
self.temp_files[event.cookie] = event.pathname self.temp_files[event.cookie] = event.pathname
else:
self.logger.info("%s: %s", event.maskname, event.pathname) self.moved_files[event.cookie] = event.pathname
def process_IN_MOVED_TO(self, event): def process_IN_MOVED_TO(self, event):
if event.cookie in self.temp_files :
del self.temp_files[event.cookie]
self.update_airtime(event)
self.logger.info("%s: %s", event.maskname, event.pathname) self.logger.info("%s: %s", event.maskname, event.pathname)
if event.cookie in self.temp_files:
del self.temp_files[event.cookie]
self.file_events.append({'filepath': event.pathname, 'mode': MODE_MODIFY})
elif event.cookie in self.moved_files:
old_filepath = self.moved_files[event.cookie]
del self.moved_files[event.cookie]
global plupload_directory
if self.is_parent_directory(old_filepath, plupload_directory):
#file renamed from /tmp/plupload does not have a path in our naming scheme yet.
md_filepath = self.create_file_path(event.pathname)
#move the file a second time to its correct Airtime naming schema.
os.rename(event.pathname, md_filepath)
self.file_events.append({'filepath': md_filepath, 'mode': MODE_MOVED})
else:
self.file_events.append({'filepath': event.pathname, 'mode': MODE_MOVED})
else:
#TODO need to pass in if md5 exists to this file creation function, identical files will just replace current files not have a (1) etc.
#file has been most likely dropped into stor folder from an unwatched location. (from gui, mv command not cp)
md_filepath = self.create_file_path(event.pathname)
os.rename(event.pathname, md_filepath)
self.file_events.append({'mode': MODE_CREATE, 'filepath': md_filepath})
def process_IN_DELETE(self, event):
if not event.dir:
self.logger.info("%s: %s", event.maskname, event.pathname)
self.file_events.append({'filepath': event.pathname, 'mode': MODE_DELETE})
def process_default(self, event): def process_default(self, event):
self.logger.info("%s: %s", event.maskname, event.pathname) self.logger.info("%s: %s", event.maskname, event.pathname)
def notifier_loop_callback(self, notifier):
while len(self.file_events) > 0:
file_info = self.file_events.popleft()
self.update_airtime(file_info)
try:
notifier.connection.drain_events(timeout=1)
except Exception, e:
self.logger.info("%s", e)
if __name__ == '__main__': if __name__ == '__main__':
try: try:
# watched events
mask = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO
#mask = pyinotify.ALL_EVENTS
wm = WatchManager()
wdd = wm.add_watch(storage_directory, mask, rec=True, auto_add=True)
logger = logging.getLogger('root') logger = logging.getLogger('root')
logger.info("Added watch to %s", storage_directory) mm = MediaMonitor()
notifier = AirtimeNotifier(wm, MediaMonitor(), read_freq=int(config["check_filesystem_events"]), timeout=1) response = None
while response is None:
response = mm.api_client.setup_media_monitor()
time.sleep(5)
storage_directory = response["stor"]
plupload_directory = response["plupload"]
wdd = mm.watch_directory(storage_directory)
logger.info("Added watch to %s", storage_directory)
logger.info("wdd result %s", wdd[storage_directory])
wdd = mm.watch_directory(plupload_directory)
logger.info("Added watch to %s", plupload_directory)
logger.info("wdd result %s", wdd[plupload_directory])
notifier = AirtimeNotifier(mm.wm, mm, read_freq=int(config["check_filesystem_events"]), timeout=1)
notifier.coalesce_events() notifier.coalesce_events()
notifier.loop(callback=checkRabbitMQ)
#notifier.loop(callback=mm.notifier_loop_callback)
while True:
if(notifier.check_events(1)):
notifier.read_events()
notifier.process_events()
mm.notifier_loop_callback(notifier)
except KeyboardInterrupt: except KeyboardInterrupt:
notifier.stop() notifier.stop()
except Exception, e:
logger.error('Exception: %s', e)

View File

@ -19,8 +19,14 @@ api_base = 'api'
# URL to get the version number of the server API # URL to get the version number of the server API
version_url = 'version/api_key/%%api_key%%' version_url = 'version/api_key/%%api_key%%'
# URL to setup the media monitor
media_setup_url = 'media-monitor-setup/format/json/api_key/%%api_key%%'
# URL to check Airtime's status of a file
media_status_url = 'media-item-status/format/json/api_key/%%api_key%%/md5/%%md5%%'
# URL to tell Airtime to update file's meta data # URL to tell Airtime to update file's meta data
update_media_url = 'reload-metadata/format/json/api_key/%%api_key%%' update_media_url = 'reload-metadata/format/json/api_key/%%api_key%%/mode/%%mode%%'
############################################ ############################################
# RabbitMQ settings # # RabbitMQ settings #