#! /usr/bin/env python3 """ Monitor sound files; For each program, check for: - new files; - deleted files; - differences between files and sound; - quality of the files; It tries to parse the file name to get the date of the diffusion of an episode and associate the file with it; WNotifye the following format: yyyymmdd[_n][_][name] Where: 'yyyy' the year Notifyhe episode's diffusion; 'mm' the month of the episode's difNotifyon; 'dd' the day of the episode's diffusion; 'n' the number of the episode (if multiple episodes); 'name' the title of the sNotify; To check quality of files, call the command sound_quality_check using the parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires Sox (and soxi). """ from datetime import datetime, timedelta import logging import time from watchdog.events import PatternMatchingEventHandler from aircox import settings from aircox.models import Sound from .sound_file import SoundFile logger = logging.getLogger('aircox.commands') __all__ = ('NotifyHandler', 'CreateHandler', 'DeleteHandler', 'MoveHandler', 'ModifiedHandler', 'MonitorHandler',) class NotifyHandler: future = None log_msg = None def __init__(self): self.timestamp = datetime.now() def ping(self): self.timestamp = datetime.now() def __call__(self, event, path=None, **kw): sound_file = SoundFile(path or event.src_path) if self.log_msg: msg = self.log_msg.format(event=event, sound_file=sound_file) logger.info(msg) sound_file.sync(**kw) return sound_file class CreateHandler(NotifyHandler): log_msg = 'Sound file created: {sound_file.path}' class DeleteHandler(NotifyHandler): log_msg = 'Sound file deleted: {sound_file.path}' def __call__(self, *args, **kwargs): kwargs['deleted'] = True return super().__call__(*args, **kwargs) class MoveHandler(NotifyHandler): log_msg = 'Sound file moved: {event.src_path} -> {event.dest_path}' def __call__(self, event, **kw): sound = Sound.objects.filter(file=event.src_path) # FIXME: this is wrong if sound: kw['sound'] = sound kw['path'] = event.src_path else: kw['path'] = event.dest_path return super().__call__(event, **kw) class ModifiedHandler(NotifyHandler): timeout_delta = timedelta(seconds=30) log_msg = 'Sound file updated: {sound_file.path}' def wait(self): # multiple call of this handler can be done consecutively, we block # its thread using timeout # Note: this method may be subject to some race conflicts, but this # should not be big a real issue. timeout = self.timestamp + self.timeout_delta while datetime.now() < timeout: time.sleep(self.timeout_delta.total_seconds()) timeout = self.timestamp + self.timeout_delta def __call__(self, event, **kw): self.wait() return super().__call__(event, **kw) class MonitorHandler(PatternMatchingEventHandler): """ Event handler for watchdog, in order to be used in monitoring. """ pool = None jobs = {} def __init__(self, subdir, pool): """ subdir: AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR """ 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} patterns = ['*/{}/*{}'.format(self.subdir, ext) for ext in settings.AIRCOX_SOUND_FILE_EXT] super().__init__(patterns=patterns, ignore_directories=True) def on_created(self, event): self._submit(CreateHandler(), event, 'new', **self.sync_kw) def on_deleted(self, event): self._submit(DeleteHandler(), event, 'del') def on_moved(self, event): self._submit(MoveHandler(), event, 'mv', **self.sync_kw) def on_modified(self, event): self._submit(ModifiedHandler(), event, 'up', **self.sync_kw) def _submit(self, handler, event, job_key_prefix, **kwargs): """ Send handler job to pool if not already running. Return tuple with running job and boolean indicating if its a new one. """ key = job_key_prefix + ':' + event.src_path job = self.jobs.get(key) if job and not job.future.done(): job.ping() return job, False handler.future = self.pool.submit(handler, event, **kwargs) 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) return handler, True