diff --git a/aircox/admin/sound.py b/aircox/admin/sound.py index 15df74f..9152699 100644 --- a/aircox/admin/sound.py +++ b/aircox/admin/sound.py @@ -70,12 +70,12 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin): if obj.type != Sound.TYPE_REMOVED else '' 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['init_app'] = True context['init_el'] = '#inline-tracks' 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): context = context or {} diff --git a/aircox/management/commands/sounds_monitor.py b/aircox/management/commands/sounds_monitor.py index 01a987a..4df098b 100755 --- a/aircox/management/commands/sounds_monitor.py +++ b/aircox/management/commands/sounds_monitor.py @@ -111,8 +111,12 @@ class Command(BaseCommand): def monitor(self): """ Run in monitor mode """ with futures.ThreadPoolExecutor() as pool: - archives_handler = MonitorHandler(settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, pool) - excerpts_handler = MonitorHandler(settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, pool) + archives_handler = MonitorHandler( + 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.schedule(archives_handler, settings.AIRCOX_PROGRAMS_DIR_ABS, diff --git a/aircox/management/sound_file.py b/aircox/management/sound_file.py index 23c9275..9c5de8f 100644 --- a/aircox/management/sound_file.py +++ b/aircox/management/sound_file.py @@ -61,12 +61,13 @@ class SoundFile: def episode(self): 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. """ if deleted: - return self._on_delete(self.path) + return self._on_delete(self.path, keep_deleted) # FIXME: sound.program as not null if not program: @@ -99,14 +100,18 @@ class SoundFile: self.find_playlist(sound) return sound - def _on_delete(self, path): + def _on_delete(self, path, keep_deleted): # TODO: remove from db on delete - sound = Sound.objects.path(self.path).first() - if sound: - sound.type = sound.TYPE_REMOVED - sound.check_on_file() - sound.save() - return sound + if keep_deleted: + sound = Sound.objects.path(self.path).first() + if sound: + if keep_deleted: + sound.type = sound.TYPE_REMOVED + sound.check_on_file() + sound.save() + return sound + else: + Sound.objects.path(self.path).delete() def read_path(self, path): """ @@ -144,7 +149,12 @@ class SoundFile: def read_file_info(self): """ 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): """ diff --git a/aircox/management/sound_monitor.py b/aircox/management/sound_monitor.py index 4beaffb..ac906d7 100644 --- a/aircox/management/sound_monitor.py +++ b/aircox/management/sound_monitor.py @@ -114,16 +114,16 @@ class MonitorHandler(PatternMatchingEventHandler): pool = None 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.pool = pool - if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR: - self.sync_kw = {'type': Sound.TYPE_ARCHIVE} - else: - self.sync_kw = {'type': Sound.TYPE_EXCERPT} + self.sync_kw = sync_kw patterns = ['*/{}/*{}'.format(self.subdir, ext) for ext in settings.AIRCOX_SOUND_FILE_EXT] @@ -156,7 +156,6 @@ class MonitorHandler(PatternMatchingEventHandler): self.jobs[key] = handler def done(r): - print(':::: job done', key) if self.jobs.get(key) is handler: del self.jobs[key] handler.future.add_done_callback(done) diff --git a/aircox/settings.py b/aircox/settings.py index 56b3377..8fb5614 100755 --- a/aircox/settings.py +++ b/aircox/settings.py @@ -130,6 +130,13 @@ ensure( ('.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 diff --git a/aircox/tests/management/sound_monitor.py b/aircox/tests/management/sound_monitor.py index 1ba2f07..0495f5c 100644 --- a/aircox/tests/management/sound_monitor.py +++ b/aircox/tests/management/sound_monitor.py @@ -62,7 +62,7 @@ class MonitorHandlerTestCase(TestCase): def test_submit_new_job(self): event = FakeEvent(src_path='dummy_src') handler = NotifyHandler() - result = self.monitor._submit(handler, event, 'up') + result, _ = self.monitor._submit(handler, event, 'up') self.assertIs(handler, result) self.assertIsInstance(handler.future, futures.Future) self.monitor.pool.shutdown() @@ -70,7 +70,9 @@ class MonitorHandlerTestCase(TestCase): def test_submit_job_exists(self): event = FakeEvent(src_path='dummy_src') - job_1 = self.monitor._submit(WaitHandler(), event, 'up') - job_2 = self.monitor._submit(NotifyHandler(), event, 'up') + job_1, new_1 = self.monitor._submit(WaitHandler(), event, 'up') + job_2, new_2 = self.monitor._submit(NotifyHandler(), event, 'up') self.assertIs(job_1, job_2) + self.assertTrue(new_1) + self.assertFalse(new_2) self.monitor.pool.shutdown()