diff --git a/aircox_programs/README.md b/aircox_programs/README.md new file mode 100644 index 0000000..9bdccf8 --- /dev/null +++ b/aircox_programs/README.md @@ -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. + diff --git a/aircox_programs/admin.py b/aircox_programs/admin.py index 54d67dd..10827df 100755 --- a/aircox_programs/admin.py +++ b/aircox_programs/admin.py @@ -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' ] } ) ] diff --git a/aircox_programs/management/commands/diffusions_monitor.py b/aircox_programs/management/commands/diffusions_monitor.py index b26db03..4fcf262 100644 --- a/aircox_programs/management/commands/diffusions_monitor.py +++ b/aircox_programs/management/commands/diffusions_monitor.py @@ -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): diff --git a/aircox_programs/management/commands/sounds_monitor.py b/aircox_programs/management/commands/sounds_monitor.py index 5392f9e..0f72d27 100644 --- a/aircox_programs/management/commands/sounds_monitor.py +++ b/aircox_programs/management/commands/sounds_monitor.py @@ -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 ) diff --git a/aircox_programs/models.py b/aircox_programs/models.py index 6fef55f..f10cb9c 100755 --- a/aircox_programs/models.py +++ b/aircox_programs/models.py @@ -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: diff --git a/aircox_programs/requirements.txt b/aircox_programs/requirements.txt new file mode 100644 index 0000000..0ff759d --- /dev/null +++ b/aircox_programs/requirements.txt @@ -0,0 +1,5 @@ +Django>=1.9.0 +django-taggit>=0.12.1 +django-autocomplete-light>=2.2.5 +django-suit>=0.2.14 + diff --git a/aircox_programs/settings.py b/aircox_programs/settings.py index f3fe104..8d4ac4d 100755 --- a/aircox_programs/settings.py +++ b/aircox_programs/settings.py @@ -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, } ) diff --git a/aircox_programs/tests.py b/aircox_programs/tests.py index 6b491ad..18b8f92 100755 --- a/aircox_programs/tests.py +++ b/aircox_programs/tests.py @@ -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