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:
parent
c3b5104f69
commit
2039579061
22
aircox_programs/README.md
Normal file
22
aircox_programs/README.md
Normal 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.
|
||||
|
|
@ -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' ] } )
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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:
|
||||
|
|
5
aircox_programs/requirements.txt
Normal file
5
aircox_programs/requirements.txt
Normal 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
|
||||
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user