Merge branch 'cc-3936-3' into devel
This commit is contained in:
commit
1a13731216
|
@ -31,6 +31,7 @@ define('MDATA_KEY_TRACKNUMBER', 'track_number');
|
||||||
define('MDATA_KEY_CONDUCTOR', 'conductor');
|
define('MDATA_KEY_CONDUCTOR', 'conductor');
|
||||||
define('MDATA_KEY_LANGUAGE', 'language');
|
define('MDATA_KEY_LANGUAGE', 'language');
|
||||||
define('MDATA_KEY_REPLAYGAIN', 'replay_gain');
|
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_FILE', 'File');
|
||||||
define('UI_MDATA_VALUE_FORMAT_STREAM', 'live stream');
|
define('UI_MDATA_VALUE_FORMAT_STREAM', 'live stream');
|
||||||
|
|
|
@ -122,9 +122,14 @@ class Application_Model_StoredFile
|
||||||
$p_md["MDATA_KEY_YEAR"] = $year;
|
$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) {
|
foreach ($p_md as $mdConst => $mdValue) {
|
||||||
if (defined($mdConst)) {
|
if (defined($mdConst)) {
|
||||||
$dbMd[constant($mdConst)] = $mdValue;
|
$dbMd[constant($mdConst)] = $mdValue;
|
||||||
|
} else {
|
||||||
|
Logging::info("Warning: using metadata that is not defined.
|
||||||
|
[$mdConst] => [$mdValue]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->setDbColMetadata($dbMd);
|
$this->setDbColMetadata($dbMd);
|
||||||
|
@ -150,10 +155,9 @@ class Application_Model_StoredFile
|
||||||
if(!$owner) { // no owner detected, we try to assign one.
|
if(!$owner) { // no owner detected, we try to assign one.
|
||||||
// if MDATA_OWNER_ID is not set then we default to the
|
// if MDATA_OWNER_ID is not set then we default to the
|
||||||
// first admin user we find
|
// 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::getUsers(array('A'));
|
||||||
$admins = Application_Model_User::getUsersOfType('A');
|
$admins = Application_Model_User::getUsersOfType('A');
|
||||||
//$admins = array();
|
|
||||||
if (count($admins) > 0) { // found admin => pick first one
|
if (count($admins) > 0) { // found admin => pick first one
|
||||||
$owner = $admins[0];
|
$owner = $admins[0];
|
||||||
}
|
}
|
||||||
|
@ -183,7 +187,6 @@ class Application_Model_StoredFile
|
||||||
if (isset($this->_dbMD[$dbColumn])) {
|
if (isset($this->_dbMD[$dbColumn])) {
|
||||||
$propelColumn = $this->_dbMD[$dbColumn];
|
$propelColumn = $this->_dbMD[$dbColumn];
|
||||||
$method = "set$propelColumn";
|
$method = "set$propelColumn";
|
||||||
Logging::info($method);
|
|
||||||
$this->_file->$method($mdValue);
|
$this->_file->$method($mdValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ class EventContractor(Loggable):
|
||||||
some other event in the storage was morphed into this newer one.
|
some other event in the storage was morphed into this newer one.
|
||||||
Which should mean that the old event should be discarded.
|
Which should mean that the old event should be discarded.
|
||||||
"""
|
"""
|
||||||
self.logger.info("Attempting to register: '%s'" % str(evt))
|
|
||||||
if self.event_registered(evt):
|
if self.event_registered(evt):
|
||||||
old_e = self.get_old_event(evt)
|
old_e = self.get_old_event(evt)
|
||||||
# TODO : Perhaps there are other events that we can "contract"
|
# TODO : Perhaps there are other events that we can "contract"
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
import abc
|
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.pure import LazyProperty
|
||||||
from media.monitor.metadata import Metadata
|
from media.monitor.metadata import Metadata
|
||||||
from media.monitor.log import Loggable
|
from media.monitor.log import Loggable
|
||||||
from media.monitor.exceptions import BadSongFile
|
from media.monitor.exceptions import BadSongFile
|
||||||
|
|
||||||
class PathChannel(object):
|
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):
|
def __init__(self, signal, path):
|
||||||
self.signal = signal
|
self.signal = signal
|
||||||
self.path = path
|
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):
|
class EventRegistry(object):
|
||||||
"""
|
"""
|
||||||
This class's main use is to keep track all events with a cookie attribute.
|
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._raw_event = raw_event
|
||||||
self.path = os.path.normpath(raw_event.pathname)
|
self.path = os.path.normpath(raw_event.pathname)
|
||||||
else: self.path = raw_event
|
else: self.path = raw_event
|
||||||
|
self.owner = owners.get_owner(self.path)
|
||||||
self._pack_hook = lambda: None # no op
|
self._pack_hook = lambda: None # no op
|
||||||
# into another event
|
# into another event
|
||||||
|
|
||||||
def reset_hook(self):
|
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()
|
||||||
self._pack_hook = lambda: None
|
self._pack_hook = lambda: None
|
||||||
|
|
||||||
|
@ -85,11 +98,12 @@ class BaseEvent(Loggable):
|
||||||
events that must catch their own BadSongFile exceptions since generate
|
events that must catch their own BadSongFile exceptions since generate
|
||||||
a set of exceptions instead of a single one
|
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:
|
try:
|
||||||
self._pack_hook()
|
self._pack_hook()
|
||||||
ret = self.pack()
|
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
|
return ret
|
||||||
except BadSongFile as e: return [e]
|
except BadSongFile as e: return [e]
|
||||||
|
|
||||||
|
@ -100,8 +114,18 @@ class BaseEvent(Loggable):
|
||||||
self.path = evt.path
|
self.path = evt.path
|
||||||
self.__class__ = evt.__class__
|
self.__class__ = evt.__class__
|
||||||
# We don't transfer the _pack_hook over to the new event
|
# 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
|
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):
|
class FakePyinotify(object):
|
||||||
"""
|
"""
|
||||||
sometimes we must create our own pyinotify like objects to
|
sometimes we must create our own pyinotify like objects to
|
||||||
|
@ -111,12 +135,20 @@ class FakePyinotify(object):
|
||||||
def __init__(self, path): self.pathname = path
|
def __init__(self, path): self.pathname = path
|
||||||
|
|
||||||
class OrganizeFile(BaseEvent, HasMetaData):
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(OrganizeFile, self).__init__(*args, **kwargs)
|
super(OrganizeFile, self).__init__(*args, **kwargs)
|
||||||
def pack(self):
|
def pack(self):
|
||||||
raise AttributeError("You can't send organize events to airtime!!!")
|
raise AttributeError("You can't send organize events to airtime!!!")
|
||||||
|
|
||||||
class NewFile(BaseEvent, HasMetaData):
|
class NewFile(BaseEvent, HasMetaData):
|
||||||
|
"""
|
||||||
|
NewFile events are the only events that contain MDATA_KEY_OWNER_ID metadata
|
||||||
|
in them.
|
||||||
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(NewFile, self).__init__(*args, **kwargs)
|
super(NewFile, self).__init__(*args, **kwargs)
|
||||||
def pack(self):
|
def pack(self):
|
||||||
|
@ -125,10 +157,16 @@ class NewFile(BaseEvent, HasMetaData):
|
||||||
"""
|
"""
|
||||||
req_dict = self.metadata.extract()
|
req_dict = self.metadata.extract()
|
||||||
req_dict['mode'] = u'create'
|
req_dict['mode'] = u'create'
|
||||||
|
self.assign_owner(req_dict)
|
||||||
req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path )
|
req_dict['MDATA_KEY_FILEPATH'] = unicode( self.path )
|
||||||
return [req_dict]
|
return [req_dict]
|
||||||
|
|
||||||
class DeleteFile(BaseEvent):
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DeleteFile, self).__init__(*args, **kwargs)
|
super(DeleteFile, self).__init__(*args, **kwargs)
|
||||||
def pack(self):
|
def pack(self):
|
||||||
|
@ -161,6 +199,10 @@ class ModifyFile(BaseEvent, HasMetaData):
|
||||||
return [req_dict]
|
return [req_dict]
|
||||||
|
|
||||||
def map_events(directory, constructor):
|
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
|
# -unknown-path should not appear in the path here but more testing
|
||||||
# might be necessary
|
# might be necessary
|
||||||
for f in mmp.walk_supported(directory, clean_empties=False):
|
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
|
except BadSongFile as e: yield e
|
||||||
|
|
||||||
class DeleteDir(BaseEvent):
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DeleteDir, self).__init__(*args, **kwargs)
|
super(DeleteDir, self).__init__(*args, **kwargs)
|
||||||
def pack(self):
|
def pack(self):
|
||||||
return map_events( self.path, DeleteFile )
|
return map_events( self.path, DeleteFile )
|
||||||
|
|
||||||
class MoveDir(BaseEvent):
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(MoveDir, self).__init__(*args, **kwargs)
|
super(MoveDir, self).__init__(*args, **kwargs)
|
||||||
def pack(self):
|
def pack(self):
|
||||||
return map_events( self.path, MoveFile )
|
return map_events( self.path, MoveFile )
|
||||||
|
|
||||||
class DeleteDirWatch(BaseEvent):
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DeleteDirWatch, self).__init__(*args, **kwargs)
|
super(DeleteDirWatch, self).__init__(*args, **kwargs)
|
||||||
def pack(self):
|
def pack(self):
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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.handler import ReportHandler
|
||||||
from media.monitor.log import Loggable
|
from media.monitor.log import Loggable
|
||||||
from media.monitor.exceptions import BadSongFile
|
from media.monitor.exceptions import BadSongFile
|
||||||
|
from media.monitor.events import OrganizeFile
|
||||||
|
|
||||||
class Organizer(ReportHandler,Loggable):
|
class Organizer(ReportHandler,Loggable):
|
||||||
"""
|
"""
|
||||||
|
@ -38,6 +40,8 @@ class Organizer(ReportHandler,Loggable):
|
||||||
directory and place it in the correct path (starting with
|
directory and place it in the correct path (starting with
|
||||||
self.target_path)
|
self.target_path)
|
||||||
"""
|
"""
|
||||||
|
# Only handle this event type
|
||||||
|
assert isinstance(event, OrganizeFile) == True
|
||||||
try:
|
try:
|
||||||
# We must select the target_path based on whether file was recorded
|
# We must select the target_path based on whether file was recorded
|
||||||
# by airtime or not.
|
# by airtime or not.
|
||||||
|
@ -47,6 +51,7 @@ class Organizer(ReportHandler,Loggable):
|
||||||
new_path = mmp.organized_path(event.path, target_path,
|
new_path = mmp.organized_path(event.path, target_path,
|
||||||
event.metadata.extract())
|
event.metadata.extract())
|
||||||
mmp.magic_move(event.path, new_path)
|
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"' %
|
self.logger.info('Organized: "%s" into "%s"' %
|
||||||
(event.path, new_path))
|
(event.path, new_path))
|
||||||
except BadSongFile as e:
|
except BadSongFile as e:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -418,6 +418,26 @@ def sub_path(directory,f):
|
||||||
common = os.path.commonprefix([ normalized, normpath(f) ])
|
common = os.path.commonprefix([ normalized, normpath(f) ])
|
||||||
return common == normalized
|
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__':
|
if __name__ == '__main__':
|
||||||
import doctest
|
import doctest
|
||||||
doctest.testmod()
|
doctest.testmod()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -82,4 +82,16 @@ class TestMMP(unittest.TestCase):
|
||||||
self.assertEqual( mmp.parse_int("123asf"), "123" )
|
self.assertEqual( mmp.parse_int("123asf"), "123" )
|
||||||
self.assertEqual( mmp.parse_int("asdf"), None )
|
self.assertEqual( mmp.parse_int("asdf"), None )
|
||||||
|
|
||||||
|
def test_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()
|
if __name__ == '__main__': unittest.main()
|
||||||
|
|
Loading…
Reference in New Issue