forked from rc/aircox
		
	Merge pull request 'Sound Monitor' (#83) from dev-1.0-sound-monitor-fixes into develop-1.0
Reviewed-on: rc/aircox#83
This commit is contained in:
		@ -70,12 +70,12 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
 | 
				
			|||||||
                if obj.type != Sound.TYPE_REMOVED else ''
 | 
					                if obj.type != Sound.TYPE_REMOVED else ''
 | 
				
			||||||
    audio.short_description = _('Audio')
 | 
					    audio.short_description = _('Audio')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_view(self, request, object_id, form_url='', context=None):
 | 
					    def add_view(self, request, form_url='', context=None):
 | 
				
			||||||
        context = context or {}
 | 
					        context = context or {}
 | 
				
			||||||
        context['init_app'] = True
 | 
					        context['init_app'] = True
 | 
				
			||||||
        context['init_el'] = '#inline-tracks'
 | 
					        context['init_el'] = '#inline-tracks'
 | 
				
			||||||
        context['track_timestamp'] = True
 | 
					        context['track_timestamp'] = True
 | 
				
			||||||
        return super().change_view(request, object_id, form_url, context)
 | 
					        return super().add_view(request, form_url, context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def change_view(self, request, object_id, form_url='', context=None):
 | 
					    def change_view(self, request, object_id, form_url='', context=None):
 | 
				
			||||||
        context = context or {}
 | 
					        context = context or {}
 | 
				
			||||||
 | 
				
			|||||||
@ -111,8 +111,12 @@ class Command(BaseCommand):
 | 
				
			|||||||
    def monitor(self):
 | 
					    def monitor(self):
 | 
				
			||||||
        """ Run in monitor mode """
 | 
					        """ Run in monitor mode """
 | 
				
			||||||
        with futures.ThreadPoolExecutor() as pool:
 | 
					        with futures.ThreadPoolExecutor() as pool:
 | 
				
			||||||
            archives_handler = MonitorHandler(settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, pool)
 | 
					            archives_handler = MonitorHandler(
 | 
				
			||||||
            excerpts_handler = MonitorHandler(settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, pool)
 | 
					                settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, pool,
 | 
				
			||||||
 | 
					                type=Sound.TYPE_ARCHIVE)
 | 
				
			||||||
 | 
					            excerpts_handler = MonitorHandler(
 | 
				
			||||||
 | 
					                settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, pool,
 | 
				
			||||||
 | 
					                type=Sound.TYPE_EXCERPT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            observer = Observer()
 | 
					            observer = Observer()
 | 
				
			||||||
            observer.schedule(archives_handler, settings.AIRCOX_PROGRAMS_DIR_ABS,
 | 
					            observer.schedule(archives_handler, settings.AIRCOX_PROGRAMS_DIR_ABS,
 | 
				
			||||||
 | 
				
			|||||||
@ -61,12 +61,13 @@ class SoundFile:
 | 
				
			|||||||
    def episode(self):
 | 
					    def episode(self):
 | 
				
			||||||
        return self.sound and self.sound.episode
 | 
					        return self.sound and self.sound.episode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def sync(self, sound=None, program=None, deleted=False, **kwargs):
 | 
					    def sync(self, sound=None, program=None, deleted=False, keep_deleted=False,
 | 
				
			||||||
 | 
					             **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Update related sound model and save it.
 | 
					        Update related sound model and save it.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if deleted:
 | 
					        if deleted:
 | 
				
			||||||
            return self._on_delete(self.path)
 | 
					            return self._on_delete(self.path, keep_deleted)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # FIXME: sound.program as not null
 | 
					        # FIXME: sound.program as not null
 | 
				
			||||||
        if not program:
 | 
					        if not program:
 | 
				
			||||||
@ -99,14 +100,18 @@ class SoundFile:
 | 
				
			|||||||
        self.find_playlist(sound)
 | 
					        self.find_playlist(sound)
 | 
				
			||||||
        return sound
 | 
					        return sound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _on_delete(self, path):
 | 
					    def _on_delete(self, path, keep_deleted):
 | 
				
			||||||
        # TODO: remove from db on delete
 | 
					        # TODO: remove from db on delete
 | 
				
			||||||
 | 
					        if keep_deleted:
 | 
				
			||||||
            sound = Sound.objects.path(self.path).first()
 | 
					            sound = Sound.objects.path(self.path).first()
 | 
				
			||||||
            if sound:
 | 
					            if sound:
 | 
				
			||||||
 | 
					                if keep_deleted:
 | 
				
			||||||
                    sound.type = sound.TYPE_REMOVED
 | 
					                    sound.type = sound.TYPE_REMOVED
 | 
				
			||||||
                    sound.check_on_file()
 | 
					                    sound.check_on_file()
 | 
				
			||||||
                    sound.save()
 | 
					                    sound.save()
 | 
				
			||||||
            return sound
 | 
					            return sound
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            Sound.objects.path(self.path).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def read_path(self, path):
 | 
					    def read_path(self, path):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -144,7 +149,12 @@ class SoundFile:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def read_file_info(self):
 | 
					    def read_file_info(self):
 | 
				
			||||||
        """ Read file information and metadata. """
 | 
					        """ Read file information and metadata. """
 | 
				
			||||||
        return mutagen.File(self.path) if os.path.exists(self.path) else None
 | 
					        try:
 | 
				
			||||||
 | 
					            if os.path.exists(self.path):
 | 
				
			||||||
 | 
					                return mutagen.File(self.path)
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def find_episode(self, sound, path_info):
 | 
					    def find_episode(self, sound, path_info):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
				
			|||||||
@ -114,16 +114,16 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
				
			|||||||
    pool = None
 | 
					    pool = None
 | 
				
			||||||
    jobs = {}
 | 
					    jobs = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, subdir, pool):
 | 
					    def __init__(self, subdir, pool, **sync_kw):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        subdir: AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR
 | 
					        :param str subdir: sub-directory in program dirs to monitor \
 | 
				
			||||||
 | 
					            (AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR);
 | 
				
			||||||
 | 
					        :param concurrent.futures.Executor pool: pool executing jobs on file change;
 | 
				
			||||||
 | 
					        :param **sync_kw: kwargs passed to `SoundFile.sync`;
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.subdir = subdir
 | 
					        self.subdir = subdir
 | 
				
			||||||
        self.pool = pool
 | 
					        self.pool = pool
 | 
				
			||||||
        if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
 | 
					        self.sync_kw = sync_kw
 | 
				
			||||||
            self.sync_kw = {'type': Sound.TYPE_ARCHIVE}
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            self.sync_kw = {'type': Sound.TYPE_EXCERPT}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        patterns = ['*/{}/*{}'.format(self.subdir, ext)
 | 
					        patterns = ['*/{}/*{}'.format(self.subdir, ext)
 | 
				
			||||||
                    for ext in settings.AIRCOX_SOUND_FILE_EXT]
 | 
					                    for ext in settings.AIRCOX_SOUND_FILE_EXT]
 | 
				
			||||||
@ -156,7 +156,6 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
				
			|||||||
        self.jobs[key] = handler
 | 
					        self.jobs[key] = handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def done(r):
 | 
					        def done(r):
 | 
				
			||||||
            print(':::: job done', key)
 | 
					 | 
				
			||||||
            if self.jobs.get(key) is handler:
 | 
					            if self.jobs.get(key) is handler:
 | 
				
			||||||
                del self.jobs[key]
 | 
					                del self.jobs[key]
 | 
				
			||||||
        handler.future.add_done_callback(done)
 | 
					        handler.future.add_done_callback(done)
 | 
				
			||||||
 | 
				
			|||||||
@ -130,6 +130,13 @@ ensure(
 | 
				
			|||||||
    ('.ogg', '.flac', '.wav', '.mp3', '.opus')
 | 
					    ('.ogg', '.flac', '.wav', '.mp3', '.opus')
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Tag sounds as deleted instead of deleting them when file has been removed
 | 
				
			||||||
 | 
					# from filesystem (sound monitoring)
 | 
				
			||||||
 | 
					ensure(
 | 
				
			||||||
 | 
					    'AIRCOX_SOUND_KEEP_DELETED',
 | 
				
			||||||
 | 
					    False
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
########################################################################
 | 
					########################################################################
 | 
				
			||||||
# Streamer & Controllers
 | 
					# Streamer & Controllers
 | 
				
			||||||
 | 
				
			|||||||
@ -62,7 +62,7 @@ class MonitorHandlerTestCase(TestCase):
 | 
				
			|||||||
    def test_submit_new_job(self):
 | 
					    def test_submit_new_job(self):
 | 
				
			||||||
        event = FakeEvent(src_path='dummy_src')
 | 
					        event = FakeEvent(src_path='dummy_src')
 | 
				
			||||||
        handler = NotifyHandler()
 | 
					        handler = NotifyHandler()
 | 
				
			||||||
        result = self.monitor._submit(handler, event, 'up')
 | 
					        result, _ = self.monitor._submit(handler, event, 'up')
 | 
				
			||||||
        self.assertIs(handler, result)
 | 
					        self.assertIs(handler, result)
 | 
				
			||||||
        self.assertIsInstance(handler.future, futures.Future)
 | 
					        self.assertIsInstance(handler.future, futures.Future)
 | 
				
			||||||
        self.monitor.pool.shutdown()
 | 
					        self.monitor.pool.shutdown()
 | 
				
			||||||
@ -70,7 +70,9 @@ class MonitorHandlerTestCase(TestCase):
 | 
				
			|||||||
    def test_submit_job_exists(self):
 | 
					    def test_submit_job_exists(self):
 | 
				
			||||||
        event = FakeEvent(src_path='dummy_src')
 | 
					        event = FakeEvent(src_path='dummy_src')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        job_1 = self.monitor._submit(WaitHandler(), event, 'up')
 | 
					        job_1, new_1 = self.monitor._submit(WaitHandler(), event, 'up')
 | 
				
			||||||
        job_2 = self.monitor._submit(NotifyHandler(), event, 'up')
 | 
					        job_2, new_2 = self.monitor._submit(NotifyHandler(), event, 'up')
 | 
				
			||||||
        self.assertIs(job_1, job_2)
 | 
					        self.assertIs(job_1, job_2)
 | 
				
			||||||
 | 
					        self.assertTrue(new_1)
 | 
				
			||||||
 | 
					        self.assertFalse(new_2)
 | 
				
			||||||
        self.monitor.pool.shutdown()
 | 
					        self.monitor.pool.shutdown()
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user