forked from rc/aircox
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)
|
@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' ] } )
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 )
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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
|
# 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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,15 +57,16 @@ 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):
|
||||||
|
for schedule, dates in self.schedules:
|
||||||
dates = [ tz.make_aware(date) for date in dates ]
|
dates = [ tz.make_aware(date) for date in dates ]
|
||||||
dates.sort()
|
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))
|
||||||
|
|
||||||
|
@ -79,3 +82,6 @@ class Programs (TestCase):
|
||||||
self.assertEqual(dates_, dates)
|
self.assertEqual(dates_, dates)
|
||||||
|
|
||||||
|
|
||||||
|
class
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user