diff --git a/airtime_mvc/application/configs/constants.php b/airtime_mvc/application/configs/constants.php index 4833f7d91..1d1ed55e4 100644 --- a/airtime_mvc/application/configs/constants.php +++ b/airtime_mvc/application/configs/constants.php @@ -31,6 +31,7 @@ define('MDATA_KEY_TRACKNUMBER', 'track_number'); define('MDATA_KEY_CONDUCTOR', 'conductor'); define('MDATA_KEY_LANGUAGE', 'language'); define('MDATA_KEY_REPLAYGAIN', 'replay_gain'); +define('MDATA_KEY_OWNER_ID', 'owner_id'); define('UI_MDATA_VALUE_FORMAT_FILE', 'File'); define('UI_MDATA_VALUE_FORMAT_STREAM', 'live stream'); diff --git a/airtime_mvc/application/models/StoredFile.php b/airtime_mvc/application/models/StoredFile.php index da01d8c54..70c804c92 100644 --- a/airtime_mvc/application/models/StoredFile.php +++ b/airtime_mvc/application/models/StoredFile.php @@ -122,9 +122,14 @@ class Application_Model_StoredFile $p_md["MDATA_KEY_YEAR"] = $year; } + # Translate metadata attributes from media monitor (MDATA_KEY_*) + # to their counterparts in constants.php (usually the column names) foreach ($p_md as $mdConst => $mdValue) { if (defined($mdConst)) { $dbMd[constant($mdConst)] = $mdValue; + } else { + Logging::info("Warning: using metadata that is not defined. + [$mdConst] => [$mdValue]"); } } $this->setDbColMetadata($dbMd); @@ -150,10 +155,9 @@ class Application_Model_StoredFile if(!$owner) { // no owner detected, we try to assign one. // if MDATA_OWNER_ID is not set then we default to the // first admin user we find - if (!array_key_exists('MDATA_OWNER_ID', $p_md)) { + if (!array_key_exists('MDATA_KEY_OWNER_ID', $p_md)) { //$admins = Application_Model_User::getUsers(array('A')); $admins = Application_Model_User::getUsersOfType('A'); - //$admins = array(); if (count($admins) > 0) { // found admin => pick first one $owner = $admins[0]; } @@ -183,7 +187,6 @@ class Application_Model_StoredFile if (isset($this->_dbMD[$dbColumn])) { $propelColumn = $this->_dbMD[$dbColumn]; $method = "set$propelColumn"; - Logging::info($method); $this->_file->$method($mdValue); } } diff --git a/python_apps/media-monitor2/media/monitor/eventcontractor.py b/python_apps/media-monitor2/media/monitor/eventcontractor.py index 089b30a44..7fa13b8cc 100644 --- a/python_apps/media-monitor2/media/monitor/eventcontractor.py +++ b/python_apps/media-monitor2/media/monitor/eventcontractor.py @@ -29,7 +29,6 @@ class EventContractor(Loggable): some other event in the storage was morphed into this newer one. Which should mean that the old event should be discarded. """ - self.logger.info("Attempting to register: '%s'" % str(evt)) if self.event_registered(evt): old_e = self.get_old_event(evt) # TODO : Perhaps there are other events that we can "contract" diff --git a/python_apps/media-monitor2/media/monitor/events.py b/python_apps/media-monitor2/media/monitor/events.py index 43ae883f8..660ff4c40 100644 --- a/python_apps/media-monitor2/media/monitor/events.py +++ b/python_apps/media-monitor2/media/monitor/events.py @@ -1,17 +1,24 @@ # -*- coding: utf-8 -*- import os import abc -import media.monitor.pure as mmp +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 class PathChannel(object): + """ + Simple struct to hold a 'signal' string and a related 'path'. Basically + used as a named tuple + """ def __init__(self, signal, path): self.signal = signal self.path = path +# 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. @@ -56,10 +63,16 @@ 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._pack_hook = lambda: None # no op # into another event 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. + """ self._pack_hook() self._pack_hook = lambda: None @@ -85,11 +98,12 @@ class BaseEvent(Loggable): events that must catch their own BadSongFile exceptions since generate a set of exceptions instead of a single one """ - # pack will only throw an exception if it processes one file but this - # is a little bit hacky 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) return ret except BadSongFile as e: return [e] @@ -100,8 +114,18 @@ class BaseEvent(Loggable): self.path = evt.path self.__class__ = evt.__class__ # We don't transfer the _pack_hook over to the new event + # TODO : perhaps we should call the old events pack_hook just to make + # sure everything is done cleanly? 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. + """ + if self.owner != -1: req['MDATA_KEY_OWNER_ID'] = self.owner + class FakePyinotify(object): """ sometimes we must create our own pyinotify like objects to @@ -111,12 +135,20 @@ class FakePyinotify(object): 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. + """ 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. + """ def __init__(self, *args, **kwargs): super(NewFile, self).__init__(*args, **kwargs) def pack(self): @@ -125,10 +157,16 @@ class NewFile(BaseEvent, HasMetaData): """ req_dict = self.metadata.extract() req_dict['mode'] = u'create' + self.assign_owner(req_dict) req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path ) 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). + """ def __init__(self, *args, **kwargs): super(DeleteFile, self).__init__(*args, **kwargs) def pack(self): @@ -161,6 +199,10 @@ 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. + """ # -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): @@ -169,18 +211,30 @@ 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. + """ 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. + """ 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 + """ def __init__(self, *args, **kwargs): super(DeleteDirWatch, self).__init__(*args, **kwargs) def pack(self): diff --git a/python_apps/media-monitor2/media/monitor/organizer.py b/python_apps/media-monitor2/media/monitor/organizer.py index 7bab09fd5..869b1a485 100644 --- a/python_apps/media-monitor2/media/monitor/organizer.py +++ b/python_apps/media-monitor2/media/monitor/organizer.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- -import media.monitor.pure as mmp +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 class Organizer(ReportHandler,Loggable): """ @@ -38,6 +40,8 @@ class Organizer(ReportHandler,Loggable): directory and place it in the correct path (starting with self.target_path) """ + # Only handle this event type + assert isinstance(event, OrganizeFile) == True try: # We must select the target_path based on whether file was recorded # by airtime or not. @@ -47,6 +51,7 @@ class Organizer(ReportHandler,Loggable): new_path = mmp.organized_path(event.path, target_path, event.metadata.extract()) mmp.magic_move(event.path, new_path) + owners.add_file_owner(new_path, mmp.owner_id(event.path) ) self.logger.info('Organized: "%s" into "%s"' % (event.path, new_path)) except BadSongFile as e: diff --git a/python_apps/media-monitor2/media/monitor/owners.py b/python_apps/media-monitor2/media/monitor/owners.py new file mode 100644 index 000000000..9d9c043bf --- /dev/null +++ b/python_apps/media-monitor2/media/monitor/owners.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from media.monitor.log import get_logger +log = get_logger() +# hash: 'filepath' => owner_id +owners = {} + +def reset_owners(): + """ + Wipes out all file => owner associations + """ + global owners + owners = {} + + +def get_owner(f): + """ + Get the owner id of the file 'f' + """ + return owners[f] if f in owners else -1 + + +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] + return True + else: return False + diff --git a/python_apps/media-monitor2/media/monitor/pure.py b/python_apps/media-monitor2/media/monitor/pure.py index fa4aa7480..bf6975068 100644 --- a/python_apps/media-monitor2/media/monitor/pure.py +++ b/python_apps/media-monitor2/media/monitor/pure.py @@ -418,6 +418,26 @@ def sub_path(directory,f): common = os.path.commonprefix([ normalized, normpath(f) ]) return common == normalized +def owner_id(original_path): + """ + Given 'original_path' return the file name of the of 'identifier' file. + return the id that is contained in it. If no file is found or nothing is + read then -1 is returned. File is deleted after the number has been read + """ + fname = "%s.identifier" % original_path + owner_id = -1 + try: + f = open(fname) + for line in f: + owner_id = int(line) + break + f.close() + except Exception: pass + else: + try: os.unlink(fname) + except Exception: raise + return owner_id + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/python_apps/media-monitor2/tests/test_owners.py b/python_apps/media-monitor2/tests/test_owners.py new file mode 100644 index 000000000..04788cf4c --- /dev/null +++ b/python_apps/media-monitor2/tests/test_owners.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import unittest +import media.monitor.owners as owners + +class TestMMP(unittest.TestCase): + def setUp(self): + self.f = "test.mp3" + + def test_has_owner(self): + owners.reset_owners() + o = 12345 + self.assertTrue( owners.add_file_owner(self.f,o) ) + self.assertTrue( owners.has_owner(self.f) ) + + def test_add_file_owner(self): + owners.reset_owners() + self.assertFalse( owners.add_file_owner('testing', -1) ) + self.assertTrue( owners.add_file_owner(self.f, 123) ) + self.assertTrue( owners.add_file_owner(self.f, 123) ) + self.assertTrue( owners.add_file_owner(self.f, 456) ) + + def test_remove_file_owner(self): + owners.reset_owners() + self.assertTrue( owners.add_file_owner(self.f, 123) ) + self.assertTrue( owners.remove_file_owner(self.f) ) + self.assertFalse( owners.remove_file_owner(self.f) ) + + def test_get_owner(self): + owners.reset_owners() + self.assertTrue( owners.add_file_owner(self.f, 123) ) + self.assertEqual( owners.get_owner(self.f), 123, "file is owned" ) + self.assertEqual( owners.get_owner("random_stuff.txt"), -1, + "file is not owned" ) + +if __name__ == '__main__': unittest.main() + diff --git a/python_apps/media-monitor2/tests/test_pure.py b/python_apps/media-monitor2/tests/test_pure.py index 59f88dabe..b4e870023 100644 --- a/python_apps/media-monitor2/tests/test_pure.py +++ b/python_apps/media-monitor2/tests/test_pure.py @@ -82,4 +82,16 @@ class TestMMP(unittest.TestCase): self.assertEqual( mmp.parse_int("123asf"), "123" ) self.assertEqual( mmp.parse_int("asdf"), None ) + def test_owner_id(self): + start_path = "testing.mp3" + id_path = "testing.mp3.identifier" + o_id = 123 + f = open(id_path, 'w') + f.write("123") + f.close() + possible_id = mmp.owner_id(start_path) + self.assertFalse( os.path.exists(id_path) ) + self.assertEqual( possible_id, o_id ) + self.assertEqual( -1, mmp.owner_id("something.random") ) + if __name__ == '__main__': unittest.main()