#! /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). """ import logging import time from datetime import datetime, timedelta 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, **sync_kw): """ :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 self.sync_kw = sync_kw 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): if self.jobs.get(key) is handler: del self.jobs[key] handler.future.add_done_callback(done) return handler, True