work on sound monitor; cast Sound.duration into Integer; handle add/remove cases even when no -s option is given to sounds_monitor

This commit is contained in:
bkfox 2015-11-02 22:34:36 +01:00
parent c3b5104f69
commit 2039579061
8 changed files with 133 additions and 71 deletions

22
aircox_programs/README.md Normal file
View File

@ -0,0 +1,22 @@
This application defines all base models and basic control of them. We have:
* **Nameable**: generic class used in any class needing to be named. Includes some utility functions;
* **Program**: the program itself;
* **Episode**: occurence of a program;
* **Diffusion**: diffusion of an episode in the timetable, linked to an episode (an episode can have multiple diffusions);
* **Schedule**: describes diffusions frequencies for each program;
* **Track**: track informations in a playlist of an episode;
* **Sound**: information about a sound that can be used for podcast or rerun;
# Program
Each program has a directory in **AIRCOX_PROGRAMS_DATA**; For each, subdir:
* **archives**: complete episode record, can be used for diffusions or as a podcast
* **excerpts**: excerpt of an episode, or other elements, can be used as a podcast
Each program has a schedule, defined through multiple schedule elements. This schedule can calculate the next dates of diffusion, if is a rerun (of wich diffusion), etc.
Basically, for each program created, we can define some options, a directory in **AIRCOX_PROGRAMS_DATA**, where subfolders defines some informations about a file.
# Notes
We don't give any view on what should be now, because it is up to the stream generator to give info about what is running.

View File

@ -50,9 +50,11 @@ class NameableAdmin (admin.ModelAdmin):
@admin.register(Sound) @admin.register(Sound)
class SoundAdmin (NameableAdmin): class SoundAdmin (NameableAdmin):
fields = None fields = None
list_display = ['id', 'name', 'duration', 'type', 'date', 'good_quality', 'removed', 'public']
fieldsets = [ fieldsets = [
(None, { 'fields': NameableAdmin.fields + ['path' ] } ), (None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ),
(None, { 'fields': ['duration', 'date', 'fragment' ] } ) (None, { 'fields': ['embed', 'duration', 'date'] }),
(None, { 'fields': ['removed', 'good_quality', 'public' ] } )
] ]

View File

@ -32,7 +32,7 @@ class Actions:
print('total of {} diffusions will be created. To be used, they need ' print('total of {} diffusions will be created. To be used, they need '
'manual approval.'.format(len(items))) 'manual approval.'.format(len(items)))
print(Diffusion.objects.bulk_create(items)) Diffusion.objects.bulk_create(items)
@staticmethod @staticmethod
def clean (date): def clean (date):

View File

@ -7,14 +7,14 @@ Monitor sound files; For each program, check for:
It tries to parse the file name to get the date of the diffusion of an It tries to parse the file name to get the date of the diffusion of an
episode and associate the file with it; We use the following format: episode and associate the file with it; We use the following format:
yyyymmdd[_n][_][title] yyyymmdd[_n][_][name]
Where: Where:
'yyyy' is the year of the episode's diffusion; 'yyyy' the year of the episode's diffusion;
'mm' is the month of the episode's diffusion; 'mm' the month of the episode's diffusion;
'dd' is the day of the episode's diffusion; 'dd' the day of the episode's diffusion;
'n' is the number of the episode (if multiple episodes); 'n' the number of the episode (if multiple episodes);
'title' the title of the sound; 'name' the title of the sound;
To check quality of files, call the command sound_quality_check using the To check quality of files, call the command sound_quality_check using the
@ -25,7 +25,6 @@ import re
from argparse import RawTextHelpFormatter from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from aircox_programs.models import * from aircox_programs.models import *
import aircox_programs.settings as settings import aircox_programs.settings as settings
@ -57,7 +56,7 @@ class Command (BaseCommand):
if options.get('scan'): if options.get('scan'):
self.scan() self.scan()
if options.get('quality_check'): if options.get('quality_check'):
self.check_quality() self.check_quality(check = (not options.get('scan')) )
def get_sound_info (self, path): def get_sound_info (self, path):
""" """
@ -74,22 +73,15 @@ class Command (BaseCommand):
if not (r and r.groupdict()): if not (r and r.groupdict()):
self.report(program, path, "file path is not correct, use defaults") self.report(program, path, "file path is not correct, use defaults")
r = { r = {
'name': os.path.splitext(path) 'name': os.path.splitext(path)[0]
} }
else:
r = r.groupdict()
r['name'] = r['name'].replace('_', ' ').capitalize()
r['path'] = path r['path'] = path
return r return r
def ensure_sound (self, sound_info):
"""
Return the Sound for the given sound_info; If not found, create it
without saving it.
"""
sound = Sound.objects.filter(path = path)
if sound:
sound = sound[0]
else:
sound = Sound(path = path, title = sound_info['name'])
def find_episode (self, program, sound_info): def find_episode (self, program, sound_info):
""" """
For a given program, and sound path check if there is an episode to For a given program, and sound path check if there is an episode to
@ -111,39 +103,50 @@ class Command (BaseCommand):
diffusion = diffusion[0] diffusion = diffusion[0]
return diffusion.episode or None return diffusion.episode or None
@staticmethod
def check_sounds (qs):
# check files
for sound in qs:
if sound.check_on_file():
sound.save(check = False)
def scan (self): def scan (self):
print('scan files for all programs...') print('scan files for all programs...')
programs = Program.objects.filter() programs = Program.objects.filter()
for program in programs: for program in programs:
print('- program ', program.name) print('- program ', program.name)
path = lambda x: os.path.join(program.path, x)
self.scan_for_program( self.scan_for_program(
program, path(settings.AIRCOX_SOUND_ARCHIVES_SUBDIR), program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type = Sound.Type['archive'], type = Sound.Type['archive'],
) )
self.scan_for_program( self.scan_for_program(
program, path(settings.AIRCOX_SOUND_EXCERPTS_SUBDIR), program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
type = Sound.Type['excerpt'], type = Sound.Type['excerpt'],
) )
def scan_for_program (self, program, dir_path, **sound_kwargs): def scan_for_program (self, program, subdir, **sound_kwargs):
""" """
Scan a given directory that is associated to the given program, and Scan a given directory that is associated to the given program, and
update sounds information. update sounds information.
""" """
print(' - scan files in', dir_path) print(' - scan files in', subdir)
if not os.path.exists(dir_path): if not program.ensure_dir(subdir):
return return
subdir = os.path.join(program.path, subdir)
# new/existing sounds # new/existing sounds
for path in os.listdir(dir_path): for path in os.listdir(subdir):
path = dir_path + '/' + path path = os.path.join(subdir, path)
if not path.endswith(settings.AIRCOX_SOUNDFILE_EXT): if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT):
continue continue
sound_info = self.get_sound_info(path) sound_info = self.get_sound_info(path)
sound = self.ensure_sound(sound_info) sound = Sound.objects.get_or_create(
path = path,
defaults = { 'name': sound_info['name'] }
)[0]
sound.__dict__.update(sound_kwargs) sound.__dict__.update(sound_kwargs)
sound.save(check = False) sound.save(check = False)
@ -160,21 +163,24 @@ class Command (BaseCommand):
episode.sounds.add(sound) episode.sounds.add(sound)
episode.save() episode.save()
# check files self.check_sounds(Sound.objects.filter(path__startswith == subdir))
for sound in Sound.object.filter(path__startswith = path):
if sound.check():
sound.save(check = False)
def check_quality (self, check = False):
def check_quality (self):
""" """
Check all files where quality has been set to bad Check all files where quality has been set to bad
""" """
import sound_quality_check as quality_check import aircox_programs.management.commands.sounds_quality_check \
as quality_check
sounds = Sound.objects.filter(good_quality = False)
if check:
self.check_sounds(sounds)
files = [ sound.path for sound in sounds if not sound.removed ]
else:
files = [ sound.path for sound in sounds.filter(removed = False) ]
print('start quality check...') print('start quality check...')
files = [ sound.path
for sound in Sound.objects.filter(good_quality = False) ]
cmd = quality_check.Command() cmd = quality_check.Command()
cmd.handle( files = files, cmd.handle( files = files,
**settings.AIRCOX_SOUND_QUALITY ) **settings.AIRCOX_SOUND_QUALITY )

View File

@ -96,7 +96,7 @@ class Sound (Nameable):
path = models.FilePathField( path = models.FilePathField(
_('file'), _('file'),
path = settings.AIRCOX_PROGRAMS_DIR, path = settings.AIRCOX_PROGRAMS_DIR,
match = '*(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT) + ')$', match = r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT).replace('.', r'\.') + ')$',
recursive = True, recursive = True,
blank = True, null = True, blank = True, null = True,
) )
@ -105,9 +105,10 @@ class Sound (Nameable):
blank = True, null = True, blank = True, null = True,
help_text = _('HTML code used to embed a sound from external plateform'), help_text = _('HTML code used to embed a sound from external plateform'),
) )
duration = models.TimeField( duration = models.IntegerField(
_('duration'), _('duration'),
blank = True, null = True, blank = True, null = True,
help_text = _('duration in seconds'),
) )
date = models.DateTimeField( date = models.DateTimeField(
_('date'), _('date'),
@ -136,7 +137,7 @@ class Sound (Nameable):
""" """
mtime = os.stat(self.path).st_mtime mtime = os.stat(self.path).st_mtime
mtime = tz.datetime.fromtimestamp(mtime) mtime = tz.datetime.fromtimestamp(mtime)
return tz.make_aware(mtime, timezone.get_current_timezone()) return tz.make_aware(mtime, tz.get_current_timezone())
def file_exists (self): def file_exists (self):
return os.path.exists(self.path) return os.path.exists(self.path)
@ -144,7 +145,7 @@ class Sound (Nameable):
def check_on_file (self): def check_on_file (self):
""" """
Check sound file info again'st self, and update informations if Check sound file info again'st self, and update informations if
needed. Return True if there was changes. needed (do not save). Return True if there was changes.
""" """
if not self.file_exists(): if not self.file_exists():
if self.removed: if self.removed:
@ -152,11 +153,15 @@ class Sound (Nameable):
self.removed = True self.removed = True
return True return True
old_removed = self.removed
self.removed = False
mtime = self.get_mtime() mtime = self.get_mtime()
if self.date != mtime: if self.date != mtime:
self.date = mtime self.date = mtime
self.good_quality = False self.good_quality = False
return True return True
return old_removed != self.removed
def save (self, check = True, *args, **kwargs): def save (self, check = True, *args, **kwargs):
if check: if check:
@ -450,9 +455,25 @@ class Program (Nameable):
return os.path.join(settings.AIRCOX_PROGRAMS_DIR, return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
slugify(self.name + '_' + str(self.id)) ) slugify(self.name + '_' + str(self.id)) )
def ensure_dir (self, subdir = None):
"""
Make sur the program's dir exists (and optionally subdir). Return True
if the dir (or subdir) exists.
"""
path = self.path
if not os.path.exists(path):
os.mkdir(path)
if subdir:
path = os.path.join(path, subdir)
if not os.path.exists(path):
os.mkdir(path)
return os.path.exists(path)
def find_schedule (self, date): def find_schedule (self, date):
""" """
Return the first schedule that matches a given date Return the first schedule that matches a given date.
""" """
schedules = Schedule.objects.filter(program = self) schedules = Schedule.objects.filter(program = self)
for schedule in schedules: for schedule in schedules:

View File

@ -0,0 +1,5 @@
Django>=1.9.0
django-taggit>=0.12.1
django-autocomplete-light>=2.2.5
django-suit>=0.2.14

View File

@ -21,8 +21,8 @@ ensure('AIRCOX_SOUND_EXCERPTS_SUBDIR', 'excerpts')
# Quality attributes passed to sound_quality_check from sounds_monitor # Quality attributes passed to sound_quality_check from sounds_monitor
ensure('AIRCOX_SOUND_QUALITY', { ensure('AIRCOX_SOUND_QUALITY', {
'attribute': 'RMS lev dB', 'attribute': 'RMS lev dB',
'range': ('-18.0', '-8.0'), 'range': (-18.0, -8.0),
'sample_length': '120', 'sample_length': 120,
} }
) )

View File

@ -5,6 +5,7 @@ from django.utils import timezone as tz
from aircox_programs.models import * from aircox_programs.models import *
class Programs (TestCase): class Programs (TestCase):
def setUp (self): def setUp (self):
stream = Stream.objects.get_or_create( stream = Stream.objects.get_or_create(
@ -13,12 +14,11 @@ class Programs (TestCase):
)[0] )[0]
Program.objects.create(name = 'source', stream = stream) Program.objects.create(name = 'source', stream = stream)
Program.objects.create(name = 'microouvert', stream = stream) Program.objects.create(name = 'microouvert', stream = stream)
#Stream.objects.create(name = 'bns', type = Stream.Type['random'], priority = 1)
#Stream.objects.create(name = 'jingle', type = Stream.Type['random'] priority = 2)
#Stream.objects.create(name = 'loves', type = Stream.Type['random'], priority = 3)
pass
def test_programs_schedules (self): self.schedules = {}
self.programs = {}
def test_create_programs_schedules (self):
program = Program.objects.get(name = 'source') program = Program.objects.get(name = 'source')
sched_0 = self.create_schedule(program, 'one on two', [ sched_0 = self.create_schedule(program, 'one on two', [
@ -34,6 +34,8 @@ class Programs (TestCase):
rerun = sched_0 rerun = sched_0
) )
self.programs[program.pk] = program
program = Program.objects.get(name = 'microouvert') program = Program.objects.get(name = 'microouvert')
# special case with november first week starting on sunday # special case with november first week starting on sunday
sched_2 = self.create_schedule(program, 'first and third', [ sched_2 = self.create_schedule(program, 'first and third', [
@ -55,27 +57,31 @@ class Programs (TestCase):
print(schedule.__dict__) print(schedule.__dict__)
schedule.save() schedule.save()
self.check_schedule(schedule, dates) self.schedules[schedule.pk] = (schedule, dates)
return schedule return schedule
def check_schedule (self, schedule, dates): def test_check_schedule (self):
dates = [ tz.make_aware(date) for date in dates ] for schedule, dates in self.schedules:
dates.sort() dates = [ tz.make_aware(date) for date in dates ]
dates.sort()
# match date and weeks # match date and weeks
for date in dates: #for date in dates:
#self.assertTrue(schedule.match(date, check_time = False)) #self.assertTrue(schedule.match(date, check_time = False))
#self.assertTrue(schedule.match_week(date)) #self.assertTrue(schedule.match_week(date))
# dates # dates
dates_ = schedule.dates_of_month(dates[0]) dates_ = schedule.dates_of_month(dates[0])
dates_.sort() dates_.sort()
self.assertEqual(dates_, dates) self.assertEqual(dates_, dates)
# diffusions # diffusions
dates_ = schedule.diffusions_of_month(dates[0]) dates_ = schedule.diffusions_of_month(dates[0])
dates_ = [date_.date for date_ in dates_] dates_ = [date_.date for date_ in dates_]
dates_.sort() dates_.sort()
self.assertEqual(dates_, dates) self.assertEqual(dates_, dates)
class