diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index 2e00a4208..df2dd5419 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -343,6 +343,7 @@ class LibraryController extends Zend_Controller_Action { $params = $this->getRequest()->getParams(); + # terrible name for the method below. it does not only search files. $r = Application_Model_StoredFile::searchLibraryFiles($params); //TODO move this to the datatables row callback. diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index 73856775a..5ed298d0f 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -387,11 +387,12 @@ class PreferenceController extends Zend_Controller_Action public function rescanWatchDirectoryAction() { - $dir = Application_Model_MusicDir::getDirByPath( - $this->getRequest()->getParam("dir")); + $dir_path = $this->getRequest()->getParam('dir'); + $dir = Application_Model_MusicDir::getDirByPath($dir_path); $data = array( 'directory' => $dir->getDirectory(), 'id' => $dir->getId()); Application_Model_RabbitMq::SendMessageToMediaMonitor('rescan_watch', $data); + Logging::info("Unhiding all files belonging to:: $dir_path"); $dir->unhideFiles(); die(); # Get rid of this ugliness later } diff --git a/airtime_mvc/application/models/MusicDir.php b/airtime_mvc/application/models/MusicDir.php index 6ace56550..3607efd4d 100644 --- a/airtime_mvc/application/models/MusicDir.php +++ b/airtime_mvc/application/models/MusicDir.php @@ -448,8 +448,12 @@ SQL; public function unhideFiles() { $files = $this->_dir->getCcFiless(); + $hid = 0; foreach ($files as $file) { + $hid++; $file->setDbHidden(false); + $file->save(); } + Logging::info("unhide '$hid' files"); } } diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index f95280063..e695c404c 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -1116,7 +1116,6 @@ class Application_Model_Preference } else { /*For now we just have this hack for debugging. We should not rely on this crappy behaviour in case of failure*/ - Logging::info("Pref: $pref_param"); Logging::warn("Index $x does not exist preferences"); Logging::warn("Defaulting to identity and printing preferences"); Logging::warn($ds); diff --git a/airtime_mvc/application/models/Schedule.php b/airtime_mvc/application/models/Schedule.php index fa2a6c79b..f46bfedde 100644 --- a/airtime_mvc/application/models/Schedule.php +++ b/airtime_mvc/application/models/Schedule.php @@ -321,7 +321,7 @@ SQL; ws.description AS file_album_title, ws.length AS file_length, 't'::BOOL AS file_exists, - NULL as file_mime + ws.mime as file_mime SQL; $streamJoin = <<<SQL cc_schedule AS sched diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index 8be6cd639..074896269 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -360,7 +360,7 @@ SQL; } - // set hidden falg to true + // set hidden flag to true $this->_file->setDbHidden(true); $this->_file->save(); @@ -726,7 +726,7 @@ SQL; $plTable = "({$plSelect} FROM cc_playlist AS PL LEFT JOIN cc_subjs AS sub ON (sub.id = PL.creator_id))"; $blTable = "({$blSelect} FROM cc_block AS BL LEFT JOIN cc_subjs AS sub ON (sub.id = BL.creator_id))"; - $fileTable = "({$fileSelect} FROM cc_files AS FILES LEFT JOIN cc_subjs AS sub ON (sub.id = FILES.owner_id) WHERE file_exists = 'TRUE')"; + $fileTable = "({$fileSelect} FROM cc_files AS FILES LEFT JOIN cc_subjs AS sub ON (sub.id = FILES.owner_id) WHERE file_exists = 'TRUE' AND hidden='FALSE')"; //$fileTable = "({$fileSelect} FROM cc_files AS FILES WHERE file_exists = 'TRUE')"; $streamTable = "({$streamSelect} FROM cc_webstream AS ws LEFT JOIN cc_subjs AS sub ON (sub.id = ws.creator_id))"; $unionTable = "({$plTable} UNION {$blTable} UNION {$fileTable} UNION {$streamTable}) AS RESULTS"; diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index b5a808986..181268983 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -655,6 +655,7 @@ var AIRTIME = (function(AIRTIME){ $pl.find('.success').show(); } disableLoadingIcon(); + setTimeout(removeSuccessMsg, 5000); }); }) diff --git a/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js b/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js index c472448aa..e487c44e3 100644 --- a/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js +++ b/airtime_mvc/public/js/airtime/playlist/smart_blockbuilder.js @@ -402,8 +402,9 @@ function setupUI() { $(".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." + text: "The desired block length will not be reached if Airtime cannot find " + + "enough unique tracks to match your criteria. Enable this option if you wish to allow " + + "tracks to be added multiple times to the smart block." }, hide: { delay: 500, diff --git a/install_full/ubuntu/airtime-full-install b/install_full/ubuntu/airtime-full-install index 955ec3ac7..08596a7e9 100755 --- a/install_full/ubuntu/airtime-full-install +++ b/install_full/ubuntu/airtime-full-install @@ -25,7 +25,8 @@ echo "----------------------------------------------------" dist=`lsb_release -is` code=`lsb_release -cs` -if [ "$dist" = "Debian" ]; then +#enable squeeze backports to get lame packages +if [ "$dist" = "Debian" -a "$code" = "squeeze" ]; then set +e grep -E "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list returncode=$? diff --git a/install_full/ubuntu/airtime-full-install-nginx b/install_full/ubuntu/airtime-full-install-nginx index 7e38e34cb..de3a5b81e 100755 --- a/install_full/ubuntu/airtime-full-install-nginx +++ b/install_full/ubuntu/airtime-full-install-nginx @@ -28,7 +28,8 @@ echo "----------------------------------------------------" dist=`lsb_release -is` code=`lsb_release -cs` -if [ "$dist" -eq "Debian" ]; then +#enable squeeze backports to get lame packages +if [ "$dist" = "Debian" -a "$code" = "squeeze" ]; then grep "deb http://backports.debian.org/debian-backports squeeze-backports main" /etc/apt/sources.list if [ "$?" -ne "0" ]; then echo "deb http://backports.debian.org/debian-backports squeeze-backports main" >> /etc/apt/sources.list diff --git a/python_apps/media-monitor2/media/monitor/airtime.py b/python_apps/media-monitor2/media/monitor/airtime.py index 11436009a..19659ed21 100644 --- a/python_apps/media-monitor2/media/monitor/airtime.py +++ b/python_apps/media-monitor2/media/monitor/airtime.py @@ -13,9 +13,8 @@ from media.monitor.log import Loggable from media.monitor.syncdb import AirtimeDB from media.monitor.exceptions import DirectoryIsNotListed from media.monitor.bootstrap import Bootstrapper -from media.monitor.listeners import FileMediator -from media.saas.thread import apc +from media.saas.thread import apc, user class AirtimeNotifier(Loggable): """ @@ -192,7 +191,7 @@ class AirtimeMessageReceiver(Loggable): # request that we'd normally get form pyinotify. But right # now event contractor would take care of this sort of # thing anyway so this might not be necessary after all - FileMediator.ignore(msg['filepath']) + #user().file_mediator.ignore(msg['filepath']) os.unlink(msg['filepath']) # Verify deletion: if not os.path.exists(msg['filepath']): diff --git a/python_apps/media-monitor2/media/monitor/events.py b/python_apps/media-monitor2/media/monitor/events.py index ac16b3746..4389bf27e 100644 --- a/python_apps/media-monitor2/media/monitor/events.py +++ b/python_apps/media-monitor2/media/monitor/events.py @@ -3,18 +3,15 @@ import os import abc import re import media.monitor.pure as mmp -import media.monitor.owners as owners from media.monitor.pure import LazyProperty from media.monitor.metadata import Metadata from media.monitor.log import Loggable from media.monitor.exceptions import BadSongFile -from media.saas.thread import getsig +from media.saas.thread import getsig, user class PathChannel(object): - """ - Simple struct to hold a 'signal' string and a related 'path'. Basically - used as a named tuple - """ + """ Simple struct to hold a 'signal' string and a related 'path'. + Basically used as a named tuple """ def __init__(self, signal, path): self.signal = getsig(signal) self.path = path @@ -22,34 +19,24 @@ class PathChannel(object): # TODO : Move this to it's file. Also possible unsingleton and use it as a # simple module just like m.m.owners class EventRegistry(object): - """ - This class's main use is to keep track all events with a cookie attribute. - This is done mainly because some events must be 'morphed' into other events - because we later detect that they are move events instead of delete events. - """ - registry = {} - @staticmethod - def register(evt): EventRegistry.registry[evt.cookie] = evt - @staticmethod - def unregister(evt): del EventRegistry.registry[evt.cookie] - @staticmethod - def registered(evt): return evt.cookie in EventRegistry.registry - @staticmethod - def matching(evt): - event = EventRegistry.registry[evt.cookie] + """ This class's main use is to keep track all events with a cookie + attribute. This is done mainly because some events must be 'morphed' + into other events because we later detect that they are move events + instead of delete events. """ + def __init__(self): + self.registry = {} + def register(self,evt): self.registry[evt.cookie] = evt + def unregister(self,evt): del self.registry[evt.cookie] + def registered(self,evt): return evt.cookie in self.registry + def matching(self,evt): + event = self.registry[evt.cookie] # Want to disallow accessing the same event twice - EventRegistry.unregister(event) + self.unregister(event) return event - def __init__(self,*args,**kwargs): - raise Exception("You can instantiate this class. Must only use class \ - methods") - class EventProxy(Loggable): - """ - A container object for instances of BaseEvent (or it's subclasses) used for - event contractor - """ + """ A container object for instances of BaseEvent (or it's + subclasses) used for event contractor """ def __init__(self, orig_evt): self.orig_evt = orig_evt self.evt = orig_evt @@ -82,12 +69,10 @@ class EventProxy(Loggable): class HasMetaData(object): - """ - Any class that inherits from this class gains the metadata attribute that - loads metadata from the class's 'path' attribute. This is done lazily so - there is no performance penalty to inheriting from this and subsequent - calls to metadata are cached - """ + """ Any class that inherits from this class gains the metadata + attribute that loads metadata from the class's 'path' attribute. + This is done lazily so there is no performance penalty to inheriting + from this and subsequent calls to metadata are cached """ __metaclass__ = abc.ABCMeta @LazyProperty def metadata(self): return Metadata(self.path) @@ -102,7 +87,7 @@ class BaseEvent(Loggable): self._raw_event = raw_event self.path = os.path.normpath(raw_event.pathname) else: self.path = raw_event - self.owner = owners.get_owner(self.path) + self.owner = user().owner.get_owner(self.path) owner_re = re.search('stor/imported/(?P<owner>\d+)/', self.path) if owner_re: self.logger.info("matched path: %s" % self.path) @@ -114,11 +99,9 @@ class BaseEvent(Loggable): # TODO : delete this method later def reset_hook(self): - """ - Resets the hook that is called after an event is packed. Before - resetting the hook we execute it to make sure that whatever cleanup - operations were queued are executed. - """ + """ Resets the hook that is called after an event is packed. + Before resetting the hook we execute it to make sure that + whatever cleanup operations were queued are executed. """ self._pack_hook() self._pack_hook = lambda: None @@ -132,10 +115,8 @@ class BaseEvent(Loggable): # TODO : delete this method later def add_safe_pack_hook(self,k): - """ - adds a callable object (function) that will be called after the event - has been "safe_packed" - """ + """ adds a callable object (function) that will be called after + the event has been "safe_packed" """ self._pack_hook = k def proxify(self): @@ -143,17 +124,15 @@ class BaseEvent(Loggable): # As opposed to unsafe_pack... def safe_pack(self): - """ - returns exceptions instead of throwing them to be consistent with - events that must catch their own BadSongFile exceptions since generate - a set of exceptions instead of a single one - """ + """ returns exceptions instead of throwing them to be consistent + with events that must catch their own BadSongFile exceptions + since generate a set of exceptions instead of a single one """ try: self._pack_hook() ret = self.pack() # Remove owner of this file only after packing. Otherwise packing # will not serialize the owner correctly into the airtime request - owners.remove_file_owner(self.path) + user().owner.remove_file_owner(self.path) return ret except BadSongFile as e: return [e] except Exception as e: @@ -172,42 +151,33 @@ class BaseEvent(Loggable): return self def assign_owner(self,req): - """ - Packs self.owner to req if the owner is valid. I.e. it's not -1. This - method is used by various events that would like to pass owner as a - parameter. NewFile for example. - """ + """ Packs self.owner to req if the owner is valid. I.e. it's not + -1. This method is used by various events that would like to + pass owner as a parameter. NewFile for example. """ if self.owner != -1: req['MDATA_KEY_OWNER_ID'] = self.owner class FakePyinotify(object): - """ - sometimes we must create our own pyinotify like objects to + """ sometimes we must create our own pyinotify like objects to instantiate objects from the classes below whenever we want to turn - a single event into multiple events - """ + a single event into multiple events """ def __init__(self, path): self.pathname = path class OrganizeFile(BaseEvent, HasMetaData): - """ - The only kind of event that does support the pack protocol. It's used - internally with mediamonitor to move files in the organize directory. - """ + """ The only kind of event that does support the pack protocol. It's + used internally with mediamonitor to move files in the organize + directory. """ def __init__(self, *args, **kwargs): super(OrganizeFile, self).__init__(*args, **kwargs) def pack(self): raise AttributeError("You can't send organize events to airtime!!!") class NewFile(BaseEvent, HasMetaData): - """ - NewFile events are the only events that contain MDATA_KEY_OWNER_ID metadata - in them. - """ + """ NewFile events are the only events that contain + MDATA_KEY_OWNER_ID metadata in them. """ def __init__(self, *args, **kwargs): super(NewFile, self).__init__(*args, **kwargs) def pack(self): - """ - packs turns an event into a media monitor request - """ + """ packs turns an event into a media monitor request """ req_dict = self.metadata.extract() req_dict['mode'] = u'create' req_dict['is_record'] = self.metadata.is_recorded() @@ -216,11 +186,9 @@ class NewFile(BaseEvent, HasMetaData): return [req_dict] class DeleteFile(BaseEvent): - """ - DeleteFile event only contains the path to be deleted. No other metadata - can be or is included. (This is because this event is fired after the - deletion occurs). - """ + """ DeleteFile event only contains the path to be deleted. No other + metadata can be or is included. (This is because this event is fired + after the deletion occurs). """ def __init__(self, *args, **kwargs): super(DeleteFile, self).__init__(*args, **kwargs) def pack(self): @@ -230,9 +198,7 @@ class DeleteFile(BaseEvent): return [req_dict] class MoveFile(BaseEvent, HasMetaData): - """ - Path argument should be the new path of the file that was moved - """ + """ Path argument should be the new path of the file that was moved """ def __init__(self, *args, **kwargs): super(MoveFile, self).__init__(*args, **kwargs) def old_path(self): @@ -256,10 +222,8 @@ class ModifyFile(BaseEvent, HasMetaData): return [req_dict] def map_events(directory, constructor): - """ - Walks 'directory' and creates an event using 'constructor'. Returns a list - of the constructed events. - """ + """ Walks 'directory' and creates an event using 'constructor'. + Returns a list of the constructed events. """ # -unknown-path should not appear in the path here but more testing # might be necessary for f in mmp.walk_supported(directory, clean_empties=False): @@ -268,30 +232,25 @@ def map_events(directory, constructor): except BadSongFile as e: yield e class DeleteDir(BaseEvent): - """ - A DeleteDir event unfolds itself into a list of DeleteFile events for every - file in the directory. - """ + """ A DeleteDir event unfolds itself into a list of DeleteFile + events for every file in the directory. """ def __init__(self, *args, **kwargs): super(DeleteDir, self).__init__(*args, **kwargs) def pack(self): return map_events( self.path, DeleteFile ) class MoveDir(BaseEvent): - """ - A MoveDir event unfolds itself into a list of MoveFile events for every - file in the directory. - """ + """ A MoveDir event unfolds itself into a list of MoveFile events + for every file in the directory. """ def __init__(self, *args, **kwargs): super(MoveDir, self).__init__(*args, **kwargs) def pack(self): return map_events( self.path, MoveFile ) class DeleteDirWatch(BaseEvent): - """ - Deleting a watched directory is different from deleting any other - directory. Hence we must have a separate event to handle this case - """ + """ Deleting a watched directory is different from deleting any + other directory. Hence we must have a separate event to handle this + case """ def __init__(self, *args, **kwargs): super(DeleteDirWatch, self).__init__(*args, **kwargs) def pack(self): diff --git a/python_apps/media-monitor2/media/monitor/listeners.py b/python_apps/media-monitor2/media/monitor/listeners.py index 081d0c4a4..86b74bcea 100644 --- a/python_apps/media-monitor2/media/monitor/listeners.py +++ b/python_apps/media-monitor2/media/monitor/listeners.py @@ -6,38 +6,33 @@ from functools import wraps import media.monitor.pure as mmp from media.monitor.pure import IncludeOnly from media.monitor.events import OrganizeFile, NewFile, MoveFile, DeleteFile, \ - DeleteDir, EventRegistry, MoveDir,\ + DeleteDir, MoveDir,\ DeleteDirWatch -from media.monitor.log import Loggable, get_logger -from media.saas.thread import getsig +from media.monitor.log import Loggable +from media.saas.thread import getsig, user # Note: Because of the way classes that inherit from pyinotify.ProcessEvent # interact with constructors. you should only instantiate objects from them # using keyword arguments. For example: # OrganizeListener('watch_signal') <= wrong # OrganizeListener(signal='watch_signal') <= right -class FileMediator(object): - """ - FileMediator is used an intermediate mechanism that filters out certain - events. - """ - ignored_set = set([]) # for paths only - logger = get_logger() - - @staticmethod - def is_ignored(path): return path in FileMediator.ignored_set - @staticmethod - def ignore(path): FileMediator.ignored_set.add(path) - @staticmethod - def unignore(path): FileMediator.ignored_set.remove(path) +class FileMediator(Loggable): + # TODO : this class is not actually used. remove all references to it + # everywhere (including tests). + """ FileMediator is used an intermediate mechanism that filters out + certain events. """ + def __init__(self) : self.ignored_set = set([]) # for paths only + def is_ignored(self,path) : return path in self.ignored_set + def ignore(self, path) : self.ignored_set.add(path) + def unignore(self, path) : self.ignored_set.remove(path) def mediate_ignored(fn): @wraps(fn) def wrapped(self, event, *args,**kwargs): event.pathname = unicode(event.pathname, "utf-8") - if FileMediator.is_ignored(event.pathname): - FileMediator.logger.info("Ignoring: '%s' (once)" % event.pathname) - FileMediator.unignore(event.pathname) + if user().file_mediator.is_ignored(event.pathname): + user().file_mediator.logger.info("Ignoring: '%s' (once)" % event.pathname) + user().file_mediator.unignore(event.pathname) else: return fn(self, event, *args, **kwargs) return wrapped @@ -80,11 +75,11 @@ class StoreWatchListener(BaseListener, Loggable, pyinotify.ProcessEvent): def process_IN_CLOSE_WRITE(self, event): self.process_create(event) def process_IN_MOVED_TO(self, event): - if EventRegistry.registered(event): + if user().event_registry.registered(event): # We need this trick because we don't how to "expand" dir events # into file events until we know for sure if we deleted or moved morph = MoveDir(event) if event.dir else MoveFile(event) - EventRegistry.matching(event).morph_into(morph) + user().event_registry.matching(event).morph_into(morph) else: self.process_create(event) def process_IN_MOVED_FROM(self, event): # Is either delete dir or delete file @@ -92,7 +87,7 @@ class StoreWatchListener(BaseListener, Loggable, pyinotify.ProcessEvent): # evt can be none whenever event points that a file that would be # ignored by @IncludeOnly if hasattr(event,'cookie') and (evt != None): - EventRegistry.register(evt) + user().event_registry.register(evt) def process_IN_DELETE(self,event): self.process_delete(event) def process_IN_MOVE_SELF(self, event): if '-unknown-path' in event.pathname: diff --git a/python_apps/media-monitor2/media/monitor/organizer.py b/python_apps/media-monitor2/media/monitor/organizer.py index 6d72bee4b..17f46054c 100644 --- a/python_apps/media-monitor2/media/monitor/organizer.py +++ b/python_apps/media-monitor2/media/monitor/organizer.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- import media.monitor.pure as mmp -import media.monitor.owners as owners from media.monitor.handler import ReportHandler from media.monitor.log import Loggable from media.monitor.exceptions import BadSongFile from media.monitor.events import OrganizeFile from pydispatch import dispatcher from os.path import dirname -from media.saas.thread import getsig +from media.saas.thread import getsig, user import os.path class Organizer(ReportHandler,Loggable): @@ -75,7 +74,7 @@ class Organizer(ReportHandler,Loggable): # backwards way is bewcause we are unable to encode the owner id # into the file itself so that the StoreWatchListener listener can # detect it from the file - owners.add_file_owner(new_path, owner_id ) + user().owner.add_file_owner(new_path, owner_id ) self.logger.info('Organized: "%s" into "%s"' % (event.path, new_path)) diff --git a/python_apps/media-monitor2/media/monitor/owners.py b/python_apps/media-monitor2/media/monitor/owners.py index 2d6c4be44..48898c9aa 100644 --- a/python_apps/media-monitor2/media/monitor/owners.py +++ b/python_apps/media-monitor2/media/monitor/owners.py @@ -1,44 +1,40 @@ # -*- coding: utf-8 -*- -from media.monitor.log import get_logger -log = get_logger() -# hash: 'filepath' => owner_id -owners = {} +from media.monitor.log import Loggable -def reset_owners(): - """ Wipes out all file => owner associations """ - global owners - owners = {} +class Owner(Loggable): + def __init__(self): + # hash: 'filepath' => owner_id + self.owners = {} + + def get_owner(self,f): + """ Get the owner id of the file 'f' """ + o = self.owners[f] if f in self.owners else -1 + self.logger.info("Received owner for %s. Owner: %s" % (f, o)) + return o -def get_owner(f): - """ Get the owner id of the file 'f' """ - o = owners[f] if f in owners else -1 - log.info("Received owner for %s. Owner: %s" % (f, o)) - return o - - -def add_file_owner(f,owner): - """ Associate file f with owner. If owner is -1 then do we will not record - it because -1 means there is no owner. Returns True if f is being stored - after the function. False otherwise. """ - if owner == -1: return False - if f in owners: - if owner != owners[f]: # check for fishiness - log.info("Warning ownership of file '%s' changed from '%d' to '%d'" - % (f, owners[f], owner)) - else: return True - owners[f] = owner - return True - -def has_owner(f): - """ True if f is owned by somebody. False otherwise. """ - return f in owners - -def remove_file_owner(f): - """ Try and delete any association made with file f. Returns true if - the the association was actually deleted. False otherwise. """ - if f in owners: - del owners[f] + def add_file_owner(self,f,owner): + """ Associate file f with owner. If owner is -1 then do we will not record + it because -1 means there is no owner. Returns True if f is being stored + after the function. False otherwise. """ + if owner == -1: return False + if f in self.owners: + if owner != self.owners[f]: # check for fishiness + self.logger.info("Warning ownership of file '%s' changed from '%d' to '%d'" + % (f, self.owners[f], owner)) + else: return True + self.owners[f] = owner return True - else: return False + + def has_owner(self,f): + """ True if f is owned by somebody. False otherwise. """ + return f in self.owners + + def remove_file_owner(self,f): + """ Try and delete any association made with file f. Returns true if + the the association was actually deleted. False otherwise. """ + if f in self.owners: + del self.owners[f] + return True + else: return False diff --git a/python_apps/media-monitor2/media/monitor/toucher.py b/python_apps/media-monitor2/media/monitor/toucher.py index cd20a21f0..d94fd34f8 100644 --- a/python_apps/media-monitor2/media/monitor/toucher.py +++ b/python_apps/media-monitor2/media/monitor/toucher.py @@ -3,6 +3,7 @@ import media.monitor.pure as mmp import os from media.monitor.log import Loggable from media.monitor.exceptions import CouldNotCreateIndexFile +from media.saas.thread import InstanceInheritingThread class Toucher(Loggable): """ @@ -17,56 +18,23 @@ class Toucher(Loggable): self.path) self.logger.info(str(e)) -#http://code.activestate.com/lists/python-ideas/8982/ -from datetime import datetime +import time -import threading - -class RepeatTimer(threading.Thread): - def __init__(self, interval, callable, args=[], kwargs={}): - threading.Thread.__init__(self) - # interval_current shows number of milliseconds in currently triggered - # <tick> - self.interval_current = interval - # interval_new shows number of milliseconds for next <tick> - self.interval_new = interval +class RepeatTimer(InstanceInheritingThread): + def __init__(self, interval, callable, *args, **kwargs): + super(RepeatTimer, self).__init__() + self.interval = interval self.callable = callable self.args = args self.kwargs = kwargs - self.event = threading.Event() - self.event.set() - self.activation_dt = None - self.__timer = None - def run(self): - while self.event.is_set(): - self.activation_dt = datetime.utcnow() - self.__timer = threading.Timer(self.interval_new, - self.callable, - self.args, - self.kwargs) - self.interval_current = self.interval_new - self.__timer.start() - self.__timer.join() - - def cancel(self): - self.event.clear() - if self.__timer is not None: - self.__timer.cancel() - - def trigger(self): - self.callable(*self.args, **self.kwargs) - if self.__timer is not None: - self.__timer.cancel() - - def change_interval(self, value): - self.interval_new = value - + while True: + time.sleep(self.interval) + self.callable(*self.args, **self.kwargs) class ToucherThread(Loggable): - """ - Creates a thread that touches a file 'path' every 'interval' seconds - """ + """ Creates a thread that touches a file 'path' every 'interval' + seconds """ def __init__(self, path, interval=5): if not os.path.exists(path): try: diff --git a/python_apps/media-monitor2/media/saas/airtimeinstance.py b/python_apps/media-monitor2/media/saas/airtimeinstance.py index fb701b432..5d02cd8e4 100644 --- a/python_apps/media-monitor2/media/saas/airtimeinstance.py +++ b/python_apps/media-monitor2/media/saas/airtimeinstance.py @@ -4,6 +4,9 @@ from os.path import join from media.monitor.exceptions import NoConfigFile from media.monitor.pure import LazyProperty from media.monitor.config import MMConfig +from media.monitor.owners import Owner +from media.monitor.events import EventRegistry +from media.monitor.listeners import FileMediator from api_clients.api_client import AirtimeApiClient # poor man's phantom types... @@ -46,3 +49,20 @@ class AirtimeInstance(object): @LazyProperty def mm_config(self): return MMConfig(self.config_paths['media_monitor']) + + # NOTE to future code monkeys: + # I'm well aware that I'm using the shitty service locator pattern + # instead of normal constructor injection as I should be. The reason + # for this is that I found these issues a little too close to the + # end of my tenure. It's highly recommended to rewrite this crap + # using proper constructor injection if you ever have the time + + @LazyProperty + def owner(self): return Owner() + + @LazyProperty + def event_registry(self): return EventRegistry() + + @LazyProperty + def file_mediator(self): return FileMediator() + diff --git a/python_apps/media-monitor2/media/saas/thread.py b/python_apps/media-monitor2/media/saas/thread.py index 49a3acd6f..e2e6be158 100644 --- a/python_apps/media-monitor2/media/saas/thread.py +++ b/python_apps/media-monitor2/media/saas/thread.py @@ -1,12 +1,13 @@ import threading class UserlessThread(Exception): - def __str__(): + def __str__(self): return "Current thread: %s is not an instance of InstanceThread \ of InstanceInheritingThread" % str(threading.current_thread()) class HasUser(object): def user(self): return self._user + def assign_user(self): self._user = threading.current_thread().user() class InstanceThread(threading.Thread, HasUser): def __init__(self,user, *args, **kwargs): @@ -15,7 +16,7 @@ class InstanceThread(threading.Thread, HasUser): class InstanceInheritingThread(threading.Thread, HasUser): def __init__(self, *args, **kwargs): - self._user = threading.current_thread().user() + self.assign_user() super(InstanceInheritingThread, self).__init__(*args, **kwargs) def user(): diff --git a/python_apps/pypo/liquidsoap_scripts/ls_lib.liq b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq index 5c5919e2e..1a18fd00a 100644 --- a/python_apps/pypo/liquidsoap_scripts/ls_lib.liq +++ b/python_apps/pypo/liquidsoap_scripts/ls_lib.liq @@ -254,29 +254,15 @@ def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, de if user == "" then user_ref := "source" end - - description_ref = ref description - if description == "" then - description_ref := "N/A" - end - - genre_ref = ref genre - if genre == "" then - genre_ref := "N/A" - end - - url_ref = ref url - if url == "" then - url_ref := "N/A" - end + output.shoutcast_mono = output.shoutcast(id = "shoutcast_stream_#{stream}", host = host, port = port, password = pass, fallible = true, - url = !url_ref, - genre = !genre_ref, - name = !description_ref, + url = url, + genre = genre, + name = description, user = !user_ref, on_error = on_error, on_connect = on_connect) @@ -286,9 +272,9 @@ def output_to(output_type, type, bitrate, host, port, pass, mount_point, url, de port = port, password = pass, fallible = true, - url = !url_ref, - genre = !genre_ref, - name = !description_ref, + url = url, + genre = genre, + name = description, user = !user_ref, on_error = on_error, on_connect = on_connect) @@ -390,13 +376,6 @@ def add_skip_command(s) "skip",fun(s) -> begin log("source.skip") skip(s) end) end -dyn_out = output.icecast(%wav, - host="localhost", - port=8999, - password=stream_harbor_pass, - mount="test-harbor", - fallible=true) - def set_dynamic_source_id(id) = current_dyn_id := id string_of(!current_dyn_id) @@ -406,123 +385,159 @@ def get_dynamic_source_id() = string_of(!current_dyn_id) end +#cc-4633 -# Function to create a playlist source and output it. -def create_dynamic_source(uri) = - # The playlist source - s = audio_to_stereo(input.http(buffer=2., max=12., uri)) - # The output - active_dyn_out = dyn_out(s) +# NOTE +# A few values are hardcoded and may be dependent: +# - the delay in gracetime is linked with the buffer duration of input.http +# (delay should be a bit less than buffer) +# - crossing duration should be less than buffer length +# (at best, a higher duration will be ineffective) - # We register both source and output - # in the list of sources - dyn_sources := - list.append([(!current_dyn_id, s),(!current_dyn_id, active_dyn_out)], !dyn_sources) +# HTTP input with "restart" command that waits for "stop" to be effected +# before "start" command is issued. Optionally it takes a new URL to play, +# which makes it a convenient replacement for "url". +# In the future, this may become a core feature of the HTTP input. +# TODO If we stop and restart quickly several times in a row, +# the data bursts accumulate and create buffer overflow. +# Flushing the buffer on restart could be a good idea, but +# it would also create an interruptions while the buffer is +# refilling... on the other hand, this would avoid having to +# fade using both cross() and switch(). +def input.http_restart(~id,~initial_url="http://dummy/url") + + source = input.http(buffer=5.,max=15.,id=id,autostart=false,initial_url) + + def stopped() + "stopped" == list.hd(server.execute("#{id}.status")) + end + + server.register(namespace=id, + "restart", + usage="restart [url]", + fun (url) -> begin + if url != "" then + log(string_of(server.execute("#{id}.url #{url}"))) + end + log(string_of(server.execute("#{id}.stop"))) + add_timeout(0.5, + { if stopped() then + log(string_of(server.execute("#{id}.start"))) ; + (-1.) + else 0.5 end}) + "OK" + end) + + # Dummy output should be useless if HTTP stream is meant + # to be listened to immediately. Otherwise, apply it. + # + # output.dummy(fallible=true,source) + + source - notify([("schedule_table_id", !current_dyn_id)]) - "Done!" end +# Transitions between URL changes in HTTP streams. +def cross_http(~debug=true,~http_input_id,source) -# A function to destroy a dynamic source -def destroy_dynamic_source(id) = - # We need to find the source in the list, - # remove it and destroy it. Currently, the language - # lacks some nice operators for that so we do it - # the functional way + id = http_input_id + last_url = ref "" + change = ref false - # This function is executed on every item in the list - # of dynamic sources - def parse_list(ret, current_element) = - # ret is of the form: (matching_sources, remaining_sources) - # We extract those two: - matching_sources = fst(ret) - remaining_sources = snd(ret) - - # current_element is of the form: ("uri", source) so - # we check the first element - current_id = fst(current_element) - if current_id == id then - # In this case, we add the source to the list of - # matched sources - (list.append( [snd(current_element)], - matching_sources), - remaining_sources) - else - # In this case, we put the element in the list of remaining - # sources - (matching_sources, - list.append([current_element], - remaining_sources)) + def on_m(m) + notify_stream(m) + changed = m["source_url"] != !last_url + log("URL now #{m['source_url']} (change: #{changed})") + if changed then + if !last_url != "" then change := true end + last_url := m["source_url"] end end - - # Now we execute the function: - result = list.fold(parse_list, ([], []), !dyn_sources) - matching_sources = fst(result) - remaining_sources = snd(result) - # We store the remaining sources in dyn_sources - dyn_sources := remaining_sources + # We use both metadata and status to know about the current URL. + # Using only metadata may be more precise is crazy corner cases, + # but it's also asking too much: the metadata may not pass through + # before the crosser is instantiated. + # Using only status in crosser misses some info, eg. on first URL. + source = on_metadata(on_m,source) - # If no source matched, we return an error - if list.length(matching_sources) == 0 then - "Error: no matching sources!" - else - # We stop all sources - list.iter(source.shutdown, matching_sources) - # And return - "Done!" + cross_d = 3. + + def crosser(a,b) + url = list.hd(server.execute('#{id}.url')) + status = list.hd(server.execute('#{id}.status')) + on_m([("source_url",url)]) + if debug then + log("New track inside HTTP stream") + log(" status: #{status}") + log(" need to cross: #{!change}") + log(" remaining #{source.remaining(a)} sec before, \ + #{source.remaining(b)} sec after") + end + if !change then + change := false + # In principle one should avoid crossing on a live stream + # it'd be okay to do it here (eg. use add instead of sequence) + # because it's only once per URL, but be cautious. + sequence([fade.out(duration=cross_d,a),fade.in(b)]) + else + # This is done on tracks inside a single stream. + # Do NOT cross here or you'll gradually empty the buffer! + sequence([a,b]) + end end + + # Setting conservative=true would mess with the delayed switch below + cross(duration=cross_d,conservative=false,crosser,source) + end +# Custom fallback between http and default source with fading of +# beginning and end of HTTP stream. +# It does not take potential URL changes into account, as long as +# they do not interrupt streaming (thanks to the HTTP buffer). +def http_fallback(~http_input_id,~http,~default) + id = http_input_id + # We use a custom switching predicate to trigger switching (and thus, + # transitions) before the end of a track (rather, end of HTTP stream). + # It is complexified because we don't want to trigger switching when + # HTTP disconnects for just an instant, when changing URL: for that + # we use gracetime below. -# A function to destroy a dynamic source -def destroy_dynamic_source_all() = - # We need to find the source in the list, - # remove it and destroy it. Currently, the language - # lacks some nice operators for that so we do it - # the functional way - - # This function is executed on every item in the list - # of dynamic sources - def parse_list(ret, current_element) = - # ret is of the form: (matching_sources, remaining_sources) - # We extract those two: - matching_sources = fst(ret) - remaining_sources = snd(ret) - - # current_element is of the form: ("uri", source) so - # we check the first element - current_uri = fst(current_element) - # in this case, we add the source to the list of - # matched sources - (list.append( [snd(current_element)], - matching_sources), - remaining_sources) + def gracetime(~delay=3.,f) + last_true = ref 0. + { if f() then + last_true := gettimeofday() + true + else + gettimeofday() < !last_true+delay + end } end - - # now we execute the function: - result = list.fold(parse_list, ([], []), !dyn_sources) - matching_sources = fst(result) - remaining_sources = snd(result) - # we store the remaining sources in dyn_sources - dyn_sources := remaining_sources - - # if no source matched, we return an error - if list.length(matching_sources) == 0 then - "error: no matching sources!" - else - # we stop all sources - list.iter(source.shutdown, matching_sources) - # And return - "Done!" + def connected() + status = list.hd(server.execute("#{id}.status")) + not(list.mem(status,["polling","stopped"])) end + connected = gracetime(connected) + + def to_live(a,b) = + log("TRANSITION to live") + add(normalize=false, + [fade.initial(b),fade.final(a)]) + end + def to_static(a,b) = + log("TRANSITION to static") + sequence([fade.out(a),fade.initial(b)]) + end + + switch( + track_sensitive=false, + transitions=[to_live,to_static], + [(# make sure it is connected, and not buffering + {connected() and source.is_ready(http) and !webstream_enabled}, http), + ({true},default)]) + end - - - diff --git a/python_apps/pypo/liquidsoap_scripts/ls_script.liq b/python_apps/pypo/liquidsoap_scripts/ls_script.liq index 34e56786e..962b20b2c 100644 --- a/python_apps/pypo/liquidsoap_scripts/ls_script.liq +++ b/python_apps/pypo/liquidsoap_scripts/ls_script.liq @@ -7,11 +7,11 @@ set("server.telnet", true) set("server.telnet.port", 1234) #Dynamic source list -dyn_sources = ref [] +#dyn_sources = ref [] webstream_enabled = ref false time = ref string_of(gettimeofday()) -queue = audio_to_stereo(id="queue_src", request.equeue(id="queue", length=0.5)) +queue = audio_to_stereo(id="queue_src", mksafe(request.equeue(id="queue", length=0.5))) queue = cue_cut(queue) queue = amplify(1., override="replay_gain", queue) @@ -35,15 +35,19 @@ s2_namespace = ref '' s3_namespace = ref '' just_switched = ref false -stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20')) +#stream_harbor_pass = list.hd(get_process_lines('pwgen -s -N 1 -n 20')) %include "ls_lib.liq" -web_stream = input.harbor("test-harbor", port=8999, password=stream_harbor_pass) -web_stream = on_metadata(notify_stream, web_stream) -output.dummy(fallible=true, web_stream) +#web_stream = input.harbor("test-harbor", port=8999, password=stream_harbor_pass) +#web_stream = on_metadata(notify_stream, web_stream) +#output.dummy(fallible=true, web_stream) +http = input.http_restart(id="http") +http = cross_http(http_input_id="http",http) +stream_queue = http_fallback(http_input_id="http",http=http,default=queue) + # the crossfade function controls fade in/out queue = crossfade_airtime(queue) queue = on_metadata(notify, queue) @@ -51,10 +55,10 @@ queue = map_metadata(update=false, append_title, queue) output.dummy(fallible=true, queue) -stream_queue = switch(id="stream_queue_switch", track_sensitive=false, - transitions=[transition, transition], - [({!webstream_enabled},web_stream), - ({true}, queue)]) +#stream_queue = switch(id="stream_queue_switch", track_sensitive=false, +# transitions=[transition, transition], +# [({!webstream_enabled},web_stream), +# ({true}, queue)]) ignore(output.dummy(stream_queue, fallible=true)) @@ -92,32 +96,33 @@ server.register(namespace="dynamic_source", fun (s) -> begin log("dynamic_source.output_stop") webstream_enabled := false "disabled" end) server.register(namespace="dynamic_source", - description="Set the cc_schedule row id", + description="Set the streams cc_schedule row id", usage="id <id>", "id", fun (s) -> begin log("dynamic_source.id") set_dynamic_source_id(s) end) server.register(namespace="dynamic_source", - description="Get the cc_schedule row id", + description="Get the streams cc_schedule row id", usage="get_id", "get_id", fun (s) -> begin log("dynamic_source.get_id") get_dynamic_source_id() end) -server.register(namespace="dynamic_source", - description="Start a new dynamic source.", - usage="start <uri>", - "read_start", - fun (uri) -> begin log("dynamic_source.read_start") create_dynamic_source(uri) end) -server.register(namespace="dynamic_source", - description="Stop a dynamic source.", - usage="stop <id>", - "read_stop", - fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source(s) end) -server.register(namespace="dynamic_source", - description="Stop a dynamic source.", - usage="stop <id>", - "read_stop_all", - fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source_all() end) +#server.register(namespace="dynamic_source", +# description="Start a new dynamic source.", +# usage="start <uri>", +# "read_start", +# fun (uri) -> begin log("dynamic_source.read_start") begin_stream_read(uri) end) +#server.register(namespace="dynamic_source", +# description="Stop a dynamic source.", +# usage="stop <id>", +# "read_stop", +# fun (s) -> begin log("dynamic_source.read_stop") stop_stream_read(s) end) + +#server.register(namespace="dynamic_source", +# description="Stop a dynamic source.", +# usage="stop <id>", +# "read_stop_all", +# fun (s) -> begin log("dynamic_source.read_stop") destroy_dynamic_source_all() end) default = amplify(id="silence_src", 0.00001, noise()) default = rewrite_metadata([("artist","Airtime"), ("title", "offline")], default) diff --git a/python_apps/pypo/pypopush.py b/python_apps/pypo/pypopush.py index 0382b5e06..c0bb36ff9 100644 --- a/python_apps/pypo/pypopush.py +++ b/python_apps/pypo/pypopush.py @@ -478,8 +478,8 @@ class PypoPush(Thread): self.logger.debug(msg) tn.write(msg) - #example: dynamic_source.read_start http://87.230.101.24:80/top100station.mp3 - msg = 'dynamic_source.read_start %s\n' % media_item['uri'].encode('latin-1') + #msg = 'dynamic_source.read_start %s\n' % media_item['uri'].encode('latin-1') + msg = 'http.restart %s\n' % media_item['uri'].encode('latin-1') self.logger.debug(msg) tn.write(msg) @@ -520,7 +520,8 @@ class PypoPush(Thread): self.telnet_lock.acquire() tn = telnetlib.Telnet(LS_HOST, LS_PORT) - msg = 'dynamic_source.read_stop_all xxx\n' + #msg = 'dynamic_source.read_stop_all xxx\n' + msg = 'http.stop\n' self.logger.debug(msg) tn.write(msg) @@ -546,7 +547,8 @@ class PypoPush(Thread): tn = telnetlib.Telnet(LS_HOST, LS_PORT) #dynamic_source.stop http://87.230.101.24:80/top100station.mp3 - msg = 'dynamic_source.read_stop %s\n' % media_item['row_id'] + #msg = 'dynamic_source.read_stop %s\n' % media_item['row_id'] + msg = 'http.stop\n' self.logger.debug(msg) tn.write(msg) diff --git a/utils/airtime-test-stream.py b/utils/airtime-test-stream.py index b7d887c0d..a2ab06982 100644 --- a/utils/airtime-test-stream.py +++ b/utils/airtime-test-stream.py @@ -27,14 +27,14 @@ def printUsage(): print " -m mount (default: test) " print " -h show help menu" - + def find_liquidsoap_binary(): """ Starting with Airtime 2.0, we don't know the exact location of the Liquidsoap binary because it may have been installed through a debian package. Let's find the location of this binary. """ - + rv = subprocess.call("which airtime-liquidsoap > /dev/null", shell=True) if rv == 0: return "airtime-liquidsoap" @@ -78,7 +78,7 @@ for o, a in optlist: mount = a try: - + print "Protocol: %s " % stream_type print "Host: %s" % host print "Port: %s" % port @@ -86,35 +86,35 @@ try: print "Password: %s" % password if stream_type == "icecast": print "Mount: %s\n" % mount - + url = "http://%s:%s/%s" % (host, port, mount) print "Outputting to %s streaming server. You should be able to hear a monotonous tone on '%s'. Press ctrl-c to quit." % (stream_type, url) - + liquidsoap_exe = find_liquidsoap_binary() - + if liquidsoap_exe is None: raise Exception("Liquidsoap not found!") - + if stream_type == "icecast": command = "%s 'output.icecast(%%vorbis, host = \"%s\", port = %s, user= \"%s\", password = \"%s\", mount=\"%s\", sine())'" % (liquidsoap_exe, host, port, user, password, mount) else: command = "%s /usr/lib/airtime/pypo/bin/liquidsoap_scripts/library/pervasives.liq 'output.shoutcast(%%mp3, host=\"%s\", port = %s, user= \"%s\", password = \"%s\", sine())'" \ % (liquidsoap_exe, host, port, user, password) - + if not verbose: command += " 2>/dev/null | grep \"failed\"" else: print command - + #print command rv = subprocess.call(command, shell=True) - + #if we reach this point, it means that our subprocess exited without the user - #doing a keyboard interrupt. This means there was a problem outputting to the + #doing a keyboard interrupt. This means there was a problem outputting to the #stream server. Print appropriate message. print "There was an error with your stream configuration. Please review your configuration " + \ "and run this program again. Use the -h option for help" - + except KeyboardInterrupt, ki: print "\nExiting" except Exception, e: