sound check

This commit is contained in:
bkfox
2023-01-25 13:02:21 +01:00
parent 276e65e0b4
commit 9ec25ed109
13 changed files with 671 additions and 330 deletions

View File

@ -25,233 +25,23 @@ Sox (and soxi).
"""
from argparse import RawTextHelpFormatter
import concurrent.futures as futures
import datetime
import atexit
import logging
import os
import re
import time
import mutagen
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler, FileModifiedEvent
from django.conf import settings as conf
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone as tz
from django.utils.translation import gettext as _
from django.core.management.base import BaseCommand
from aircox import settings, utils
from aircox.models import Diffusion, Program, Sound, Track
from .import_playlist import PlaylistImport
from aircox import settings
from aircox.models import Program, Sound
from aircox.management.sound_file import SoundFile
from aircox.management.sound_monitor import MonitorHandler
logger = logging.getLogger('aircox.commands')
sound_path_re = re.compile(
'^(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})'
'(_(?P<hour>[0-9]{2})h(?P<minute>[0-9]{2}))?'
'(_(?P<n>[0-9]+))?'
'_?[ -]*(?P<name>.*)$'
)
class SoundFile:
path = None
info = None
path_info = None
sound = None
def __init__(self, path):
self.path = path
@property
def sound_path(self):
""" Relative path name """
return self.path.replace(conf.MEDIA_ROOT + '/', '')
def sync(self, sound=None, program=None, deleted=False, **kwargs):
"""
Update related sound model and save it.
"""
if deleted:
sound = Sound.objects.path(self.path).first()
if sound:
sound.type = sound.TYPE_REMOVED
sound.check_on_file()
sound.save()
return sound
# FIXME: sound.program as not null
if not program:
program = Program.get_from_path(self.path)
logger.info('program from path "%s" -> %s', self.path, program)
kwargs['program_id'] = program.pk
sound, created = Sound.objects.get_or_create(file=self.sound_path, defaults=kwargs) \
if not sound else (sound, False)
self.sound = sound
sound.program = program
if created or sound.check_on_file():
logger.info('sound is new or have been modified -> %s', self.sound_path)
self.read_path()
sound.name = self.path_info.get('name')
self.read_file_info()
if self.info is not None:
sound.duration = utils.seconds_to_time(self.info.info.length)
# check for episode
if sound.episode is None and self.read_path():
self.find_episode(program)
sound.save()
# check for playlist
self.find_playlist(sound)
return sound
def read_path(self):
"""
Parse file name to get info on the assumption it has the correct
format (given in Command.help). Return True if path contains informations.
"""
if self.path_info:
return 'year' in self.path_info
name = os.path.splitext(os.path.basename(self.path))[0]
match = sound_path_re.search(name)
if match:
path_info = match.groupdict()
for k in ('year', 'month', 'day', 'hour', 'minute'):
if path_info.get(k) is not None:
path_info[k] = int(path_info[k])
self.path_info = path_info
return True
else:
self.path_info = {'name': name}
return False
def read_file_info(self):
""" Read file information and metadata. """
if os.path.exists(self.path):
self.info = mutagen.File(self.path)
else:
self.info = None
def find_episode(self, program):
"""
For a given program, check if there is an initial diffusion
to associate to, using the date info we have. Update self.sound
and save it consequently.
We only allow initial diffusion since there should be no
rerun.
"""
pi = self.path_info
if 'year' not in pi or not self.sound or self.sound.episode:
return None
if pi.get('hour') is not None:
date = tz.datetime(pi.get('year'), pi.get('month'), pi.get('day'),
pi.get('hour') or 0, pi.get('minute') or 0)
date = tz.get_current_timezone().localize(date)
else:
date = datetime.date(pi.get('year'), pi.get('month'), pi.get('day'))
diffusion = program.diffusion_set.at(date).first()
if not diffusion:
return None
logger.info('%s <--> %s', self.sound.file.name, str(diffusion.episode))
self.sound.episode = diffusion.episode
return diffusion
def find_playlist(self, sound=None, use_meta=True):
"""
Find a playlist file corresponding to the sound path, such as:
my_sound.ogg => my_sound.csv
Use sound's file metadata if no corresponding playlist has been
found and `use_meta` is True.
"""
if sound is None:
sound = self.sound
if sound.track_set.count() > 1:
return
# import playlist
path_noext, ext = os.path.splitext(self.sound.file.path)
path = path_noext + '.csv'
if os.path.exists(path):
PlaylistImport(path, sound=sound).run()
# use metadata
elif use_meta:
if self.info is None:
self.read_file_info()
if self.info and self.info.tags:
tags = self.info.tags
title, artist, album, year = tuple(
t and ', '.join(t) for t in (
tags.get(k) for k in ('title', 'artist', 'album', 'year'))
)
title = title or (self.path_info and self.path_info.get('name')) or \
os.path.basename(path_noext)
info = '{} ({})'.format(album, year) if album and year else \
album or year or ''
track = Track(sound=sound,
position=int(tags.get('tracknumber', 0)),
title=title,
artist=artist or _('unknown'),
info=info)
track.save()
class MonitorHandler(PatternMatchingEventHandler):
"""
Event handler for watchdog, in order to be used in monitoring.
"""
pool = None
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.sound_kwargs = {'type': Sound.TYPE_ARCHIVE}
else:
self.sound_kwargs = {'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.on_modified(event)
def on_modified(self, event):
logger.info('sound modified: %s', event.src_path)
def updated(event, sound_kwargs):
SoundFile(event.src_path).sync(**sound_kwargs)
self.pool.submit(updated, event, self.sound_kwargs)
def on_moved(self, event):
logger.info('sound moved: %s -> %s', event.src_path, event.dest_path)
def moved(event, sound_kwargs):
sound = Sound.objects.filter(file=event.src_path)
sound_file = SoundFile(event.dest_path) if not sound else sound
sound_file.sync(**sound_kwargs)
self.pool.submit(moved, event, self.sound_kwargs)
def on_deleted(self, event):
logger.info('sound deleted: %s', event.src_path)
def deleted(event):
SoundFile(event.src_path).sync(deleted=True)
self.pool.submit(deleted, event)
class Command(BaseCommand):
help = __doc__