utils.py 12.9 KB
Newer Older
fred's avatar
fred committed
1
from datetime import datetime, timedelta, time
fred's avatar
fred committed
2
import os
fred's avatar
fred committed
3
import re
4
import subprocess
5 6
from PIL import Image

fred's avatar
fred committed
7 8
from django.utils.encoding import force_text

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

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:
fred's avatar
fred committed
33
            hours, minutes, seconds, cs = re.findall(r'(\d\d):(\d\d):(\d\d)\.(\d\d)', line)[0]
34 35
        except IndexError:
            continue
fred's avatar
fred committed
36
        return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + (int(cs) / 100)
37 38 39
    return None


40
def maybe_resize(image_path):
fred's avatar
fred committed
41 42
    if not os.path.exists(image_path):
        return
43 44 45 46 47 48 49 50 51 52
    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)


53
def whatsonair():
fred's avatar
fred committed
54
    from .models import Diffusion, Schedule, Nonstop
55 56

    now = datetime.now()
57 58 59
    # 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),
60
            prefetch_sounds=False, prefetch_categories=False, include_nonstop=False)
61
    program = [x for x in program if not x.datetime > now]
62 63

    emission = None
64
    episode = None
fred's avatar
fred committed
65
    nonstop = None
66 67 68 69 70 71 72 73
    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
74
    else:
fred's avatar
fred committed
75 76 77 78 79 80 81 82 83
        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
84 85

    return {'emission': emission,
86
            'episode': episode,
fred's avatar
fred committed
87
            'nonstop': nonstop,
88
            'current_slot': current_slot}
89 90


91
def period_program(date_start, date_end, prefetch_sounds=True,
92
        prefetch_categories=True, include_nonstop=True):
fred's avatar
fred committed
93
    from .models import Diffusion, Schedule, Nonstop, WeekdayMixin, SoundFile, Absence
94

95
    diffusions = Diffusion.objects.select_related().filter(
96
            datetime__range=(date_start, date_end)).order_by('datetime')
97 98
    if prefetch_categories:
        diffusions = diffusions.prefetch_related('episode__emission__categories')
99 100 101
    diffusions = [x for x in diffusions if x.datetime >= date_start and
                    x.datetime < date_end]

102 103 104 105 106 107 108 109 110
    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)

111 112 113
    # 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.
114 115 116
    period_schedules = Schedule.objects.select_related().order_by('datetime', 'weeks')
    if prefetch_categories:
        period_schedules = period_schedules.prefetch_related('emission__categories')
117 118 119 120 121 122 123 124 125 126 127 128

    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())
129 130 131 132
            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)
133 134
        program.extend(day_schedules)
        current_date += timedelta(days=1)
135

fred's avatar
fred committed
136 137 138 139
    absences = {}
    for absence in Absence.objects.filter(datetime__range=(date_start, date_end)):
        absences[absence.datetime] = True

140 141 142
    for i, schedule in enumerate(program):
        if schedule is None:
            continue
143

144
        # look for a diffusion matching this schedule
145
        d = [x for x in diffusions if x.datetime.timetuple()[:5] == schedule.datetime.timetuple()[:5]]
146
        if d:
147
            diffusions.remove(d[0])
148 149
            program[i] = d[0]
            for j, other_schedule in enumerate(program[i+1:]):
150
                # remove other emissions scheduled at the same time
151
                if other_schedule.datetime.timetuple()[:5] == schedule.datetime.timetuple()[:5]:
152 153 154 155
                    program[i+1+j] = None
                else:
                    break

fred's avatar
fred committed
156 157 158 159
        if schedule.datetime in absences and isinstance(program[i], Schedule):
            program[i] = None
            continue

160
    # here we are with remaining diffusions, those that were not overriding a
fred's avatar
fred committed
161
    # planned schedule
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
    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
193
                if (diff.seconds/60) >= next_prog.get_duration():
194 195 196 197 198 199 200
                    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
201 202 203 204 205 206 207 208 209 210 211 212 213 214
    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

fred's avatar
fred committed
215 216
    program = [x for x in program if x is not None]

217 218 219
    if not include_nonstop:
        return program

fred's avatar
fred committed
220
    # last step is adding nonstop zones between slots
221
    nonstops = list(Nonstop.objects.all().order_by('start'))
222
    nonstops = [x for x in nonstops if x.start != x.end]
223
    dawn = time(Schedule.DAY_HOUR_START, 0)
224 225 226 227 228
    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
fred's avatar
fred committed
229 230

    class NonstopSlot(WeekdayMixin):
231 232 233 234 235
        def __init__(self, nonstop, dt):
            self.datetime = dt
            self.title = nonstop.title
            self.slug = nonstop.slug
            self.label = self.title
236
            self.nonstop = nonstop
237 238

        def __repr__(self):
239
            return '<Nonstop Slot %r>' % self.label
240 241 242 243 244 245 246 247 248

        def get_duration(self):
            # fake duration
            return 0

        @classmethod
        def get_serie(cls, start, end):
            cells = []
            dt = None
249 250
            delta_day = 0
            last_added_end = None
fred's avatar
fred committed
251 252 253
            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)
254 255 256
                nonstop_day_start += timedelta(days=delta_day)
                nonstop_day_end += timedelta(days=delta_day)
                if nonstop.start > nonstop.end:
257
                    nonstop_day_end += timedelta(days=1)
fred's avatar
fred committed
258 259

                if nonstop_day_start < end and nonstop_day_end > start:
260
                    if dt is None:
261 262 263
                        dt = start
                    else:
                        dt = nonstop_day_start
264
                    cells.append(cls(nonstop, dt))
265 266 267 268 269
                    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
270 271 272
                    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))
fred's avatar
fred committed
273

274
            cells.sort(key=lambda x: x.datetime)
275
            if last_added_end and last_added_end < end:
276 277 278
                # 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))
279
            return cells
280

fred's avatar
fred committed
281 282 283 284

    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])

285
    if program:
286
        program[0:0] = NonstopSlot.get_serie(first_day_start, program[0].datetime)
fred's avatar
fred committed
287 288 289 290 291 292 293 294 295 296 297

    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:
298 299 300 301 302 303
                    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
fred's avatar
fred committed
304
                else:
305
                    program[i+1:i+1] = NonstopSlot.get_serie(slot_end, next_slot.datetime)
fred's avatar
fred committed
306 307 308

        i += 1

309
    if program:
310 311 312 313
        program.extend(
                NonstopSlot.get_serie(program[-1].datetime +
                    timedelta(minutes=program[-1].get_duration()),
                last_day_end))
fred's avatar
fred committed
314 315

    return program
316

317 318
def day_program(date, prefetch_sounds=True, prefetch_categories=True,
        include_nonstop=True):
319 320
    date_start = datetime(*date.timetuple()[:3])
    date_end = date_start + timedelta(days=1)
321 322
    return period_program(date_start, date_end,
            prefetch_sounds=prefetch_sounds,
323 324
            prefetch_categories=prefetch_categories,
            include_nonstop=include_nonstop)