add episode: models, admin, diffusion gen, sounds_monitor, playlist import

This commit is contained in:
bkfox
2019-07-29 18:58:45 +02:00
parent fff4801ac7
commit 8581743d13
95 changed files with 1976 additions and 17373 deletions

View File

@ -15,60 +15,57 @@ planified before the (given) month.
- "check" will remove all diffusions that are unconfirmed and have been planified
from the (given) month and later.
"""
import time
import datetime
import logging
from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone as tz
from aircox.models import *
from aircox.models import Schedule, Diffusion
logger = logging.getLogger('aircox.tools')
class Actions:
@classmethod
def update(cl, date, mode):
manual = (mode == 'manual')
date = None
count = [0, 0]
for schedule in Schedule.objects.filter(program__active=True) \
.order_by('initial'):
# in order to allow rerun links between diffusions, we save items
# by schedule;
items = schedule.diffusions_of_month(date, exclude_saved=True)
count[0] += len(items)
def __init__(self, date):
self.date = date or datetime.date.today()
# we can't bulk create because we need signal processing
for item in items:
conflicts = item.get_conflicts()
item.type = Diffusion.Type.unconfirmed \
if manual or conflicts.count() else \
Diffusion.Type.normal
item.save(no_check=True)
if conflicts.count():
item.conflicts.set(conflicts.all())
def update(self):
episodes, diffusions = [], []
for schedule in Schedule.objects.filter(program__active=True,
initial__isnull=True):
eps, diffs = schedule.diffusions_of_month(self.date)
logger.info('[update] schedule %s: %d new diffusions',
str(schedule), len(items),
)
episodes += eps
diffusions += diffs
logger.info('[update] %d diffusions have been created, %s', count[0],
'do not forget manual approval' if manual else
'{} conflicts found'.format(count[1]))
logger.info('[update] %s: %d episodes, %d diffusions and reruns',
str(schedule), len(eps), len(diffs))
@staticmethod
def clean(date):
with transaction.atomic():
logger.info('[update] save %d episodes and %d diffusions',
len(episodes), len(diffusions))
for episode in episodes:
episode.save()
for diffusion in diffusions:
# force episode id's update
diffusion.episode = diffusion.episode
diffusion.save()
def clean(self):
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
start__lt=date)
start__lt=self.date)
logger.info('[clean] %d diffusions will be removed', qs.count())
qs.delete()
@staticmethod
def check(date):
def check(self):
# TODO: redo
qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed,
start__gt=date)
start__gt=self.date)
items = []
for diffusion in qs:
schedules = Schedule.objects.filter(program=diffusion.program)
@ -88,21 +85,21 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.formatter_class = RawTextHelpFormatter
now = tz.datetime.today()
today = datetime.date.today()
group = parser.add_argument_group('action')
group.add_argument(
'--update', action='store_true',
'-u', '--update', action='store_true',
help='generate (unconfirmed) diffusions for the given month. '
'These diffusions must be confirmed manually by changing '
'their type to "normal"'
)
group.add_argument(
'--clean', action='store_true',
'-l', '--clean', action='store_true',
help='remove unconfirmed diffusions older than the given month'
)
group.add_argument(
'--check', action='store_true',
'-c', '--check', action='store_true',
help='check unconfirmed later diffusions from the given '
'date agains\'t schedule. If no schedule is found, remove '
'it.'
@ -110,10 +107,10 @@ class Command(BaseCommand):
group = parser.add_argument_group('date')
group.add_argument(
'--year', type=int, default=now.year,
'--year', type=int, default=today.year,
help='used by update, default is today\'s year')
group.add_argument(
'--month', type=int, default=now.month,
'--month', type=int, default=today.month,
help='used by update, default is today\'s month')
group.add_argument(
'--next-month', action='store_true',
@ -121,31 +118,20 @@ class Command(BaseCommand):
' (if next month from today'
)
group = parser.add_argument_group('options')
group.add_argument(
'--mode', type=str, choices=['manual', 'auto'],
default='auto',
help='manual means that all generated diffusions are unconfirmed, '
'thus must be approved manually; auto confirmes all '
'diffusions except those that conflicts with others'
)
def handle(self, *args, **options):
date = tz.datetime(year=options.get('year'),
month=options.get('month'),
day=1)
date = tz.make_aware(date)
date = datetime.date(year=options['year'], month=options['month'],
day=1)
if options.get('next_month'):
month = options.get('month')
date += tz.timedelta(days=28)
if date.month == month:
date += tz.timedelta(days=28)
date = date.replace(day=1)
actions = Actions(date)
if options.get('update'):
Actions.update(date, mode=options.get('mode'))
actions.update()
if options.get('clean'):
Actions.clean(date)
actions.clean()
if options.get('check'):
Actions.check(date)
actions.check()

View File

@ -1,6 +1,6 @@
"""
Import one or more playlist for the given sound. Attach it to the sound
or to the related Diffusion if wanted.
Import one or more playlist for the given sound. Attach it to the provided
sound.
Playlists are in CSV format, where columns are separated with a
'{settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
@ -18,14 +18,15 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError
from django.contrib.contenttypes.models import ContentType
from aircox import settings
from aircox.models import *
import aircox.settings as settings
__doc__ = __doc__.format(settings=settings)
logger = logging.getLogger('aircox.tools')
class Importer:
class PlaylistImport:
path = None
data = None
tracks = None
@ -121,17 +122,12 @@ class Command (BaseCommand):
help='generate a playlist for the sound of the given path. '
'If not given, try to match a sound with the same path.'
)
parser.add_argument(
'--diffusion', '-d', action='store_true',
help='try to get the diffusion relative to the sound if it exists'
)
def handle(self, path, *args, **options):
# FIXME: absolute/relative path of sounds vs given path
if options.get('sound'):
sound = Sound.objects.filter(
path__icontains=options.get('sound')
).first()
sound = Sound.objects.filter(path__icontains=options.get('sound'))\
.first()
else:
path_, ext = os.path.splitext(path)
sound = Sound.objects.filter(path__icontains=path_).first()
@ -141,11 +137,10 @@ class Command (BaseCommand):
'{path}'.format(path=path))
return
if options.get('diffusion') and sound.diffusion:
sound = sound.diffusion
importer = Importer(path, sound=sound).run()
# FIXME: auto get sound.episode if any
importer = PlaylistImport(path, sound=sound).run()
for track in importer.tracks:
logger.info('track #{pos} imported: {title}, by {artist}'.format(
pos=track.position, title=track.title, artist=track.artist
))

View File

@ -23,6 +23,7 @@ parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
Sox (and soxi).
"""
from argparse import RawTextHelpFormatter
import datetime
import atexit
import logging
import os
@ -37,13 +38,21 @@ from django.conf import settings as main_settings
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone as tz
from aircox.models import *
import aircox.settings as settings
import aircox.utils as utils
from aircox import settings, utils
from aircox.models import Diffusion, Program, Sound
from .import_playlist import PlaylistImport
logger = logging.getLogger('aircox.tools')
sound_path_re = re.compile(
'^(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})'
'(_(?P<hour>[0-9]{2})h(?P<minute>[0-9]{2}))?'
'(_(?P<n>[0-9]+))?'
'_?(?P<name>.*)$'
)
class SoundInfo:
name = ''
sound = None
@ -66,33 +75,19 @@ class SoundInfo:
Parse file name to get info on the assumption it has the correct
format (given in Command.help)
"""
file_name = os.path.basename(value)
file_name = os.path.splitext(file_name)[0]
r = re.search('^(?P<year>[0-9]{4})'
'(?P<month>[0-9]{2})'
'(?P<day>[0-9]{2})'
'(_(?P<hour>[0-9]{2})h(?P<minute>[0-9]{2}))?'
'(_(?P<n>[0-9]+))?'
'_?(?P<name>.*)$',
file_name)
if not (r and r.groupdict()):
r = {'name': file_name}
logger.info('file name can not be parsed -> %s', value)
else:
r = r.groupdict()
name = os.path.splitext(os.path.basename(value))[0]
match = sound_path_re.search(name)
match = match.groupdict() if match and match.groupdict() else \
{'name': name}
self._path = value
self.name = r['name'].replace('_', ' ').capitalize()
self.name = match['name'].replace('_', ' ').capitalize()
for key in ('year', 'month', 'day', 'hour', 'minute'):
value = r.get(key)
if value is not None:
value = int(value)
setattr(self, key, value)
value = match.get(key)
setattr(self, key, int(value) if value is not None else None)
self.n = r.get('n')
return r
self.n = match.get('n')
def __init__(self, path='', sound=None):
self.path = path
@ -116,9 +111,8 @@ class SoundInfo:
(if save is True, sync to DB), and check for a playlist file.
"""
sound, created = Sound.objects.get_or_create(
path=self.path,
defaults=kwargs
)
path=self.path, defaults=kwargs)
if created or sound.check_on_file():
logger.info('sound is new or have been modified -> %s', self.path)
sound.duration = self.get_duration()
@ -139,22 +133,17 @@ class SoundInfo:
if sound.track_set.count():
return
import aircox.management.commands.import_playlist \
as import_playlist
# no playlist, try to retrieve metadata
# import playlist
path = os.path.splitext(self.sound.path)[0] + '.csv'
if not os.path.exists(path):
if use_default:
track = sound.file_metadata()
if track:
track.save()
return
if os.path.exists(path):
PlaylistImport(path, sound=sound).run()
# try metadata
elif use_default:
track = sound.file_metadata()
if track:
track.save()
# else, import
import_playlist.Importer(path, sound=sound).run()
def find_diffusion(self, program, save=True):
def find_episode(self, program, save=True):
"""
For a given program, check if there is an initial diffusion
to associate to, using the date info we have. Update self.sound
@ -163,25 +152,22 @@ class SoundInfo:
We only allow initial diffusion since there should be no
rerun.
"""
if self.year == None or not self.sound or self.sound.diffusion:
if self.year is None or not self.sound or self.sound.episode:
return
if self.hour is None:
date = datetime.date(self.year, self.month, self.day)
else:
date = datetime.datetime(self.year, self.month, self.day,
self.hour or 0, self.minute or 0)
date = tz.datetime(self.year, self.month, self.day,
self.hour or 0, self.minute or 0)
date = tz.get_current_timezone().localize(date)
qs = Diffusion.objects.station(program.station).after(date) \
.filter(program=program, initial__isnull=True)
diffusion = qs.first()
diffusion = program.diffusion_set.initial().at(date).first()
if not diffusion:
return
logger.info('diffusion %s mathes to sound -> %s', str(diffusion),
self.sound.path)
self.sound.diffusion = diffusion
logger.info('%s <--> %s', self.sound.path, str(diffusion.episode))
self.sound.episode = diffusion.episode
if save:
self.sound.save()
return diffusion
@ -219,7 +205,7 @@ class MonitorHandler(PatternMatchingEventHandler):
self.sound_kwargs['program'] = program
si.get_sound(save=True, **self.sound_kwargs)
if si.year is not None:
si.find_diffusion(program)
si.find_episode(program)
si.sound.save(True)
def on_deleted(self, event):
@ -246,7 +232,7 @@ class MonitorHandler(PatternMatchingEventHandler):
if program:
si = SoundInfo(sound.path, sound=sound)
if si.year is not None:
si.find_diffusion(program)
si.find_episode(program)
sound.save()
@ -270,7 +256,7 @@ class Command(BaseCommand):
dirs = []
for program in programs:
logger.info('#%d %s', program.id, program.name)
logger.info('#%d %s', program.id, program.title)
self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type=Sound.Type.archive,
@ -304,7 +290,7 @@ class Command(BaseCommand):
si = SoundInfo(path)
sound_kwargs['program'] = program
si.get_sound(save=True, **sound_kwargs)
si.find_diffusion(program, save=True)
si.find_episode(program, save=True)
si.find_playlist(si.sound)
sounds.append(si.sound.pk)

View File

@ -216,7 +216,7 @@ class Monitor:
return
qs = Diffusions.objects.station(self.station).at().filter(
type=Diffusion.Type.normal,
type=Diffusion.Type.on_air,
sound__type=Sound.Type.archive,
)
logs = Log.objects.station(station).on_air().with_diff()