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)
class SoundAdmin (NameableAdmin):
fields = None
list_display = ['id', 'name', 'duration', 'type', 'date', 'good_quality', 'removed', 'public']
fieldsets = [
(None, { 'fields': NameableAdmin.fields + ['path' ] } ),
(None, { 'fields': ['duration', 'date', 'fragment' ] } )
(None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ),
(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 '
'manual approval.'.format(len(items)))
print(Diffusion.objects.bulk_create(items))
Diffusion.objects.bulk_create(items)
@staticmethod
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
episode and associate the file with it; We use the following format:
yyyymmdd[_n][_][title]
yyyymmdd[_n][_][name]
Where:
'yyyy' is the year of the episode's diffusion;
'mm' is the month of the episode's diffusion;
'dd' is the day of the episode's diffusion;
'n' is the number of the episode (if multiple episodes);
'title' the title of the sound;
'yyyy' the year of the episode's diffusion;
'mm' the month of the episode's diffusion;
'dd' the day of the episode's diffusion;
'n' the number of the episode (if multiple episodes);
'name' the title of the sound;
To check quality of files, call the command sound_quality_check using the
@ -25,7 +25,6 @@ import re
from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from aircox_programs.models import *
import aircox_programs.settings as settings
@ -57,7 +56,7 @@ class Command (BaseCommand):
if options.get('scan'):
self.scan()
if options.get('quality_check'):
self.check_quality()
self.check_quality(check = (not options.get('scan')) )
def get_sound_info (self, path):
"""
@ -74,22 +73,15 @@ class Command (BaseCommand):
if not (r and r.groupdict()):
self.report(program, path, "file path is not correct, use defaults")
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
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):
"""
For a given program, and sound path check if there is an episode to
@ -111,39 +103,50 @@ class Command (BaseCommand):
diffusion = diffusion[0]
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):
print('scan files for all programs...')
programs = Program.objects.filter()
for program in programs:
print('- program ', program.name)
path = lambda x: os.path.join(program.path, x)
self.scan_for_program(
program, path(settings.AIRCOX_SOUND_ARCHIVES_SUBDIR),
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type = Sound.Type['archive'],
)
self.scan_for_program(
program, path(settings.AIRCOX_SOUND_EXCERPTS_SUBDIR),
program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
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
update sounds information.
"""
print(' - scan files in', dir_path)
if not os.path.exists(dir_path):
print(' - scan files in', subdir)
if not program.ensure_dir(subdir):
return
subdir = os.path.join(program.path, subdir)
# new/existing sounds
for path in os.listdir(dir_path):
path = dir_path + '/' + path
if not path.endswith(settings.AIRCOX_SOUNDFILE_EXT):
for path in os.listdir(subdir):
path = os.path.join(subdir, path)
if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT):
continue
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.save(check = False)
@ -160,21 +163,24 @@ class Command (BaseCommand):
episode.sounds.add(sound)
episode.save()
# check files
for sound in Sound.object.filter(path__startswith = path):
if sound.check():
sound.save(check = False)
self.check_sounds(Sound.objects.filter(path__startswith == subdir))
def check_quality (self):
def check_quality (self, check = False):
"""
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...')
files = [ sound.path
for sound in Sound.objects.filter(good_quality = False) ]
cmd = quality_check.Command()
cmd.handle( files = files,
**settings.AIRCOX_SOUND_QUALITY )

View File

@ -96,7 +96,7 @@ class Sound (Nameable):
path = models.FilePathField(
_('file'),
path = settings.AIRCOX_PROGRAMS_DIR,
match = '*(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT) + ')$',
match = r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT).replace('.', r'\.') + ')$',
recursive = True,
blank = True, null = True,
)
@ -105,9 +105,10 @@ class Sound (Nameable):
blank = True, null = True,
help_text = _('HTML code used to embed a sound from external plateform'),
)
duration = models.TimeField(
duration = models.IntegerField(
_('duration'),
blank = True, null = True,
help_text = _('duration in seconds'),
)
date = models.DateTimeField(
_('date'),
@ -136,7 +137,7 @@ class Sound (Nameable):
"""
mtime = os.stat(self.path).st_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):
return os.path.exists(self.path)
@ -144,7 +145,7 @@ class Sound (Nameable):
def check_on_file (self):
"""
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 self.removed:
@ -152,11 +153,15 @@ class Sound (Nameable):
self.removed = True
return True
old_removed = self.removed
self.removed = False
mtime = self.get_mtime()
if self.date != mtime:
self.date = mtime
self.good_quality = False
return True
return old_removed != self.removed
def save (self, check = True, *args, **kwargs):
if check:
@ -450,9 +455,25 @@ class Program (Nameable):
return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
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):
"""
Return the first schedule that matches a given date
Return the first schedule that matches a given date.
"""
schedules = Schedule.objects.filter(program = self)
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
ensure('AIRCOX_SOUND_QUALITY', {
'attribute': 'RMS lev dB',
'range': ('-18.0', '-8.0'),
'sample_length': '120',
'range': (-18.0, -8.0),
'sample_length': 120,
}
)

View File

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