from datetime import datetime, timedelta, time import os import re import subprocess from PIL import Image from django.utils.encoding import force_text def get_duration(filename): p = subprocess.Popen(['mediainfo', '--Inform=Audio;%Duration%', filename], close_fds=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() try: return int(stdout) / 1000 except ValueError: pass # fallback on soxi p = subprocess.Popen(['soxi', filename], close_fds=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() for line in stdout.splitlines(): line = force_text(line) if not line.startswith('Duration'): continue try: hours, minutes, seconds, cs = re.findall(r'(\d\d):(\d\d):(\d\d)\.(\d\d)', line)[0] except IndexError: continue return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + (int(cs) / 100) return None def maybe_resize(image_path): if not os.path.exists(image_path): return image = Image.open(image_path) if max(image.size) > 1000: # no sense storing images that large factor = 1000. / max(image.size) image = image.resize( (int(image.size[0]*factor), int(image.size[1]*factor)), Image.ANTIALIAS) image.save(image_path) def whatsonair(): from .models import Diffusion, Schedule, Nonstop now = datetime.now() # get program of today minus a few hours, as radio days are not from # midnight to midnigth but from 5am to 5am program = day_program(now - timedelta(hours=Schedule.DAY_HOUR_START), prefetch_sounds=False, prefetch_categories=False, include_nonstop=False) program = [x for x in program if not x.datetime > now] emission = None episode = None nonstop = None current_slot = None if program and program[-1].datetime + timedelta(minutes=program[-1].get_duration()) > now: current_slot = program[-1] if isinstance(current_slot, Schedule): emission = current_slot.emission elif isinstance(current_slot, Diffusion): episode = current_slot.episode emission = episode.emission else: for nonstop in Nonstop.objects.all(): if (nonstop.start < nonstop.end and ( now.time() >= nonstop.start and now.time() < nonstop.end)) or \ (nonstop.start > nonstop.end and ( now.time() >= nonstop.start or now.time() < nonstop.end)): current_slot = nonstop break else: nonstop = None return {'emission': emission, 'episode': episode, 'nonstop': nonstop, 'current_slot': current_slot} def period_program(date_start, date_end, prefetch_sounds=True, prefetch_categories=True, include_nonstop=True): from .models import Diffusion, Schedule, Nonstop, WeekdayMixin, SoundFile, Absence diffusions = Diffusion.objects.select_related().filter( datetime__range=(date_start, date_end)).order_by('datetime') if prefetch_categories: diffusions = diffusions.prefetch_related('episode__emission__categories') diffusions = [x for x in diffusions if x.datetime >= date_start and x.datetime < date_end] if prefetch_sounds: soundfiles = {} for soundfile in SoundFile.objects.select_related().filter(podcastable=True, fragment=False, episode__in=[x.episode for x in diffusions]): soundfiles[soundfile.episode_id] = soundfile for diffusion in diffusions: diffusion.episode.main_sound = soundfiles.get(diffusion.episode.id) # the secondary sortkey puts schedules that happens everyweek after # specific ones, this will be useful later on, when multiple schedules # happen at the same time and we have to remove the least specific. period_schedules = Schedule.objects.select_related().order_by('datetime', 'weeks') if prefetch_categories: period_schedules = period_schedules.prefetch_related('emission__categories') program = [] current_date = date_start while current_date < date_end: week_day = current_date.weekday() week_no = ((current_date.day-1) // 7) day_schedules = [x for x in period_schedules if x.get_weekday() == week_day and x.match_week(week_no)] for schedule in day_schedules: schedule.datetime = datetime( current_date.year, current_date.month, current_date.day, schedule.datetime.hour, schedule.datetime.minute) + \ timedelta(days=schedule.datetime.weekday()-current_date.weekday()) if week_day == 6 and schedule.datetime.weekday() == 0: # on Sundays we can have Sunday->Monday night programming, and # we need to get them on the right date. schedule.datetime += timedelta(days=7) program.extend(day_schedules) current_date += timedelta(days=1) absences = {} for absence in Absence.objects.filter(datetime__range=(date_start, date_end)): absences[absence.datetime] = True for i, schedule in enumerate(program): if schedule is None: continue # look for a diffusion matching this schedule d = [x for x in diffusions if x.datetime.timetuple()[:5] == schedule.datetime.timetuple()[:5]] if d: diffusions.remove(d[0]) program[i] = d[0] for j, other_schedule in enumerate(program[i+1:]): # remove other emissions scheduled at the same time if other_schedule.datetime.timetuple()[:5] == schedule.datetime.timetuple()[:5]: program[i+1+j] = None else: break if schedule.datetime in absences and isinstance(program[i], Schedule): program[i] = None continue # here we are with remaining diffusions, those that were not overriding a # planned schedule for diffusion in diffusions: program = [x for x in program if x is not None] try: just_before_program = [x for x in program if x.datetime < diffusion.datetime][-1] new_diff_index = program.index(just_before_program)+1 except IndexError: # nothing before new_diff_index = 0 program.insert(new_diff_index, diffusion) # cut (or even remove) programs that started earlier but continued over # this program start time i = new_diff_index while i > 0: previous = program[i-1] previous_endtime = previous.datetime + timedelta(minutes=previous.get_duration()) if previous_endtime > diffusion.datetime: previous.duration = (diffusion.datetime - previous.datetime).seconds / 60 if previous.duration <= 0: program[i-1] = None i -= 1 # push back (or remove) programs that started before this program ends # (this may be unnecessary as next step does that again for all # programs) diffusion_endtime = diffusion.datetime + timedelta(minutes=diffusion.get_duration()) i = new_diff_index while i < len(program)-1: next_prog = program[i+1] if next_prog.datetime < diffusion_endtime: diff = diffusion_endtime - next_prog.datetime if (diff.seconds/60) >= next_prog.get_duration(): program[i+1] = None else: next_prog.datetime = diffusion_endtime next_prog.duration = next_prog.get_duration() - (diff.seconds/60) i += 1 # remove overlapping programs program = [x for x in program if x is not None] for i, slot in enumerate(program): if slot is None: continue slot_end = slot.datetime + timedelta(minutes=slot.get_duration()) j = i+1 while j < len(program)-1: if program[j]: if slot_end > program[j].datetime: program[j] = None j += 1 program = [x for x in program if x is not None] if not include_nonstop: return program # last step is adding nonstop zones between slots nonstops = list(Nonstop.objects.all().order_by('start')) nonstops = [x for x in nonstops if x.start != x.end] dawn = time(Schedule.DAY_HOUR_START, 0) try: first_of_the_day = [x for x in nonstops if x.start <= dawn][-1] nonstops = nonstops[nonstops.index(first_of_the_day):] + nonstops[:nonstops.index(first_of_the_day)] except IndexError: pass class NonstopSlot(WeekdayMixin): def __init__(self, nonstop, dt): self.datetime = dt self.title = nonstop.title self.slug = nonstop.slug self.label = self.title self.nonstop = nonstop def __repr__(self): return '' % self.label def get_duration(self): # fake duration return 0 @classmethod def get_serie(cls, start, end): cells = [] dt = None delta_day = 0 last_added_end = None for nonstop in nonstops: nonstop_day_start = start.replace(hour=nonstop.start.hour, minute=nonstop.start.minute) nonstop_day_end = start.replace(hour=nonstop.end.hour, minute=nonstop.end.minute) nonstop_day_start += timedelta(days=delta_day) nonstop_day_end += timedelta(days=delta_day) if nonstop.start > nonstop.end: nonstop_day_end += timedelta(days=1) if nonstop_day_start < end and nonstop_day_end > start: if dt is None: dt = start else: dt = nonstop_day_start cells.append(cls(nonstop, dt)) last_added_end = nonstop_day_end if nonstop.start > nonstop.end: # we just added a midnight crossing slot, future slots # should be one day later. delta_day += 1 if nonstop.start < dawn and nonstop.end > dawn and start.time() < dawn: dt = dt.replace(hour=dawn.hour, minute=dawn.minute) cells.append(cls(nonstop, dt)) cells.sort(key=lambda x: x.datetime) if last_added_end and last_added_end < end: # we used all nonstop slots and we still did not reach the end; # let's go for one more turn. cells.extend(cls.get_serie(last_added_end, end)) return cells first_day_start = datetime(*date_start.replace(hour=6, minute=0).timetuple()[:5]) last_day_end = datetime(*date_end.replace(hour=5, minute=0).timetuple()[:5]) if program: program[0:0] = NonstopSlot.get_serie(first_day_start, program[0].datetime) i = 0 while i < len(program)-1: slot = program[i] if not isinstance(slot, NonstopSlot): slot_end = slot.datetime + timedelta(minutes=slot.get_duration()) next_slot = program[i+1] if slot_end < next_slot.datetime: next_day_start = next_slot.datetime.replace(hour=5, minute=0) if slot_end < next_day_start and next_slot.datetime > next_day_start: nonstop_day_slots = NonstopSlot.get_serie(slot_end, next_day_start) nonstop_next_day_slots = NonstopSlot.get_serie(next_day_start, next_slot.datetime) if nonstop_day_slots and nonstop_next_day_slots and \ nonstop_day_slots[-1].label == nonstop_next_day_slots[0].label: nonstop_next_day_slots = nonstop_next_day_slots[1:] program[i+1:i+1] = nonstop_day_slots + nonstop_next_day_slots else: program[i+1:i+1] = NonstopSlot.get_serie(slot_end, next_slot.datetime) i += 1 if program: program.extend( NonstopSlot.get_serie(program[-1].datetime + timedelta(minutes=program[-1].get_duration()), last_day_end)) return program def day_program(date, prefetch_sounds=True, prefetch_categories=True, include_nonstop=True): date_start = datetime(*date.timetuple()[:3]) date_end = date_start + timedelta(days=1) return period_program(date_start, date_end, prefetch_sounds=prefetch_sounds, prefetch_categories=prefetch_categories, include_nonstop=include_nonstop)