]> git.0d.be Git - django-panik-emissions.git/blob - emissions/utils.py
adjust time of dawn
[django-panik-emissions.git] / emissions / utils.py
1 from datetime import datetime, timedelta, time
2 import os
3 import re
4 import subprocess
5 from PIL import Image
6
7 from django.utils.encoding import force_text
8
9
10 def get_duration(filename):
11     p = subprocess.Popen(['mediainfo', '--Inform=Audio;%Duration%', filename],
12                          close_fds=True,
13                          stdin=subprocess.PIPE,
14                          stdout=subprocess.PIPE,
15                          stderr=subprocess.PIPE)
16     stdout, stderr = p.communicate()
17     try:
18         return int(stdout) / 1000
19     except ValueError:
20         pass
21
22     # fallback on soxi
23     p = subprocess.Popen(['soxi', filename], close_fds=True,
24                          stdin=subprocess.PIPE,
25                          stdout=subprocess.PIPE,
26                          stderr=subprocess.PIPE)
27     stdout, stderr = p.communicate()
28     for line in stdout.splitlines():
29         line = force_text(line)
30         if not line.startswith('Duration'):
31             continue
32         try:
33             hours, minutes, seconds, cs = re.findall(r'(\d\d):(\d\d):(\d\d)\.(\d\d)', line)[0]
34         except IndexError:
35             continue
36         return int(hours) * 3600 + int(minutes) * 60 + int(seconds) + (int(cs) / 100)
37     return None
38
39
40 def maybe_resize(image_path):
41     if not os.path.exists(image_path):
42         return
43     image = Image.open(image_path)
44     if max(image.size) > 1000:
45         # no sense storing images that large
46         factor = 1000. / max(image.size)
47         image = image.resize(
48                 (int(image.size[0]*factor), int(image.size[1]*factor)),
49                 Image.ANTIALIAS)
50         image.save(image_path)
51
52
53 def whatsonair():
54     from .models import Diffusion, Schedule, Nonstop
55
56     now = datetime.now()
57     # get program of today minus a few hours, as radio days are not from
58     # midnight to midnigth but from 5am to 5am
59     program = day_program(now - timedelta(hours=Schedule.DAY_HOUR_START) + timedelta(minutes=Schedule.DAY_MINUTE_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
65     nonstop = None
66     current_slot = None
67     if program and program[-1].datetime + timedelta(minutes=program[-1].get_duration()) > now:
68         current_slot = program[-1]
69         if isinstance(current_slot, Schedule):
70             emission = current_slot.emission
71         elif isinstance(current_slot, Diffusion):
72             episode = current_slot.episode
73             emission = episode.emission
74     else:
75         for nonstop in Nonstop.objects.all():
76             if (nonstop.start < nonstop.end and (
77                     now.time() >= nonstop.start and now.time() < nonstop.end)) or \
78                (nonstop.start > nonstop.end and (
79                     now.time() >= nonstop.start or now.time() < nonstop.end)):
80                 current_slot = nonstop
81                 break
82         else:
83             nonstop = None
84
85     return {'emission': emission,
86             'episode': episode,
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):
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     if prefetch_categories:
98         diffusions = diffusions.prefetch_related('episode__emission__categories')
99     diffusions = [x for x in diffusions if x.datetime >= date_start and
100                     x.datetime < date_end]
101
102     if prefetch_sounds:
103         soundfiles = {}
104         for soundfile in SoundFile.objects.select_related().filter(podcastable=True,
105                 fragment=False, episode__in=[x.episode for x in diffusions]):
106             soundfiles[soundfile.episode_id] = soundfile
107
108         for diffusion in diffusions:
109             diffusion.episode.main_sound = soundfiles.get(diffusion.episode.id)
110
111     # the secondary sortkey puts schedules that happens everyweek after
112     # specific ones, this will be useful later on, when multiple schedules
113     # happen at the same time and we have to remove the least specific.
114     period_schedules = Schedule.objects.select_related().order_by('datetime', 'weeks')
115     if prefetch_categories:
116         period_schedules = period_schedules.prefetch_related('emission__categories')
117
118     program = []
119     current_date = date_start
120     while current_date < date_end:
121         week_day = current_date.weekday()
122         week_no = ((current_date.day-1) // 7)
123         day_schedules = [x for x in period_schedules if x.get_weekday() == week_day and x.match_week(week_no)]
124         for schedule in day_schedules:
125             schedule.datetime = datetime(
126                     current_date.year, current_date.month, current_date.day,
127                     schedule.datetime.hour, schedule.datetime.minute) + \
128                             timedelta(days=schedule.datetime.weekday()-current_date.weekday())
129             if week_day == 6 and schedule.datetime.weekday() == 0:
130                 # on Sundays we can have Sunday->Monday night programming, and
131                 # we need to get them on the right date.
132                 schedule.datetime += timedelta(days=7)
133         program.extend(day_schedules)
134         current_date += timedelta(days=1)
135
136     absences = {}
137     for absence in Absence.objects.filter(datetime__range=(date_start, date_end)):
138         absences[absence.datetime] = True
139
140     for i, schedule in enumerate(program):
141         if schedule is None:
142             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             program[i] = d[0]
149             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                     program[i+1+j] = None
153                 else:
154                     break
155
156         if schedule.datetime in absences and isinstance(program[i], Schedule):
157             program[i] = None
158             continue
159
160     # here we are with remaining diffusions, those that were not overriding a
161     # planned schedule
162     for diffusion in diffusions:
163         program = [x for x in program if x is not None]
164         try:
165             just_before_program = [x for x in program if x.datetime < diffusion.datetime][-1]
166             new_diff_index = program.index(just_before_program)+1
167         except IndexError:
168             # nothing before
169             new_diff_index = 0
170         program.insert(new_diff_index, diffusion)
171
172         # cut (or even remove) programs that started earlier but continued over
173         # this program start time
174         i = new_diff_index
175         while i > 0:
176             previous = program[i-1]
177             previous_endtime = previous.datetime + timedelta(minutes=previous.get_duration())
178             if previous_endtime > diffusion.datetime:
179                 previous.duration = (diffusion.datetime - previous.datetime).seconds / 60
180                 if previous.duration <= 0:
181                     program[i-1] = None
182             i -= 1
183
184         # push back (or remove) programs that started before this program ends
185         # (this may be unnecessary as next step does that again for all
186         # programs)
187         diffusion_endtime = diffusion.datetime + timedelta(minutes=diffusion.get_duration())
188         i = new_diff_index
189         while i < len(program)-1:
190             next_prog = program[i+1]
191             if next_prog.datetime < diffusion_endtime:
192                 diff = diffusion_endtime - next_prog.datetime
193                 if (diff.seconds/60) >= next_prog.get_duration():
194                     program[i+1] = None
195                 else:
196                     next_prog.datetime = diffusion_endtime
197                     next_prog.duration = next_prog.get_duration() - (diff.seconds/60)
198             i += 1
199
200     # remove overlapping programs
201     program = [x for x in program if x is not None]
202     for i, slot in enumerate(program):
203         if slot is None:
204             continue
205
206         slot_end = slot.datetime + timedelta(minutes=slot.get_duration())
207
208         j = i+1
209         while j < len(program)-1:
210             if program[j]:
211                 if slot_end > program[j].datetime:
212                     program[j] = None
213             j += 1
214
215     program = [x for x in program if x is not None]
216
217     if not include_nonstop:
218         return program
219
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, Schedule.DAY_MINUTE_START)
224     try:
225         first_of_the_day = [x for x in nonstops if x.start <= dawn][-1]
226         nonstops = nonstops[nonstops.index(first_of_the_day):] + nonstops[:nonstops.index(first_of_the_day)]
227     except IndexError:
228         pass
229
230     class NonstopSlot(WeekdayMixin):
231         def __init__(self, nonstop, dt):
232             self.datetime = dt
233             self.title = nonstop.title
234             self.slug = nonstop.slug
235             self.label = self.title
236             self.nonstop = nonstop
237
238         def __repr__(self):
239             return '<Nonstop Slot %r>' % self.label
240
241         def get_duration(self):
242             # fake duration
243             return 0
244
245         @classmethod
246         def get_serie(cls, start, end):
247             cells = []
248             dt = None
249             delta_day = 0
250             last_added_end = None
251             for nonstop in nonstops:
252                 nonstop_day_start = start.replace(hour=nonstop.start.hour, minute=nonstop.start.minute)
253                 nonstop_day_end = start.replace(hour=nonstop.end.hour, minute=nonstop.end.minute)
254                 nonstop_day_start += timedelta(days=delta_day)
255                 nonstop_day_end += timedelta(days=delta_day)
256                 if nonstop.start > nonstop.end:
257                     nonstop_day_end += timedelta(days=1)
258
259                 if nonstop_day_start < end and nonstop_day_end > start:
260                     if dt is None:
261                         dt = start
262                     else:
263                         dt = nonstop_day_start
264                     cells.append(cls(nonstop, dt))
265                     last_added_end = nonstop_day_end
266                     if nonstop.start > nonstop.end:
267                         # we just added a midnight crossing slot, future slots
268                         # should be one day later.
269                         delta_day += 1
270                     if nonstop.start < dawn and nonstop.end > dawn and start.time() < dawn:
271                         dt = dt.replace(hour=dawn.hour, minute=dawn.minute)
272                         cells.append(cls(nonstop, dt))
273
274             cells.sort(key=lambda x: x.datetime)
275             if last_added_end and last_added_end < end:
276                 # we used all nonstop slots and we still did not reach the end;
277                 # let's go for one more turn.
278                 cells.extend(cls.get_serie(last_added_end, end))
279             return cells
280
281
282     first_day_start = datetime(*date_start.replace(hour=6, minute=0).timetuple()[:5])
283     last_day_end = datetime(*date_end.replace(hour=5, minute=0).timetuple()[:5])
284
285     if program:
286         program[0:0] = NonstopSlot.get_serie(first_day_start, program[0].datetime)
287
288     i = 0
289     while i < len(program)-1:
290         slot = program[i]
291         if not isinstance(slot, NonstopSlot):
292             slot_end = slot.datetime + timedelta(minutes=slot.get_duration())
293             next_slot = program[i+1]
294
295             if slot_end < next_slot.datetime:
296                 next_day_start = next_slot.datetime.replace(hour=5, minute=0)
297                 if slot_end < next_day_start and next_slot.datetime > next_day_start:
298                     nonstop_day_slots = NonstopSlot.get_serie(slot_end, next_day_start)
299                     nonstop_next_day_slots = NonstopSlot.get_serie(next_day_start, next_slot.datetime)
300                     if nonstop_day_slots and nonstop_next_day_slots and \
301                             nonstop_day_slots[-1].label == nonstop_next_day_slots[0].label:
302                         nonstop_next_day_slots = nonstop_next_day_slots[1:]
303                     program[i+1:i+1] = nonstop_day_slots + nonstop_next_day_slots
304                 else:
305                     program[i+1:i+1] = NonstopSlot.get_serie(slot_end, next_slot.datetime)
306
307         i += 1
308
309     if program:
310         program.extend(
311                 NonstopSlot.get_serie(program[-1].datetime +
312                     timedelta(minutes=program[-1].get_duration()),
313                 last_day_end))
314
315     return program
316
317 def day_program(date, prefetch_sounds=True, prefetch_categories=True,
318         include_nonstop=True):
319     date_start = datetime(*date.timetuple()[:3])
320     date_end = date_start + timedelta(days=1)
321     return period_program(date_start, date_end,
322             prefetch_sounds=prefetch_sounds,
323             prefetch_categories=prefetch_categories,
324             include_nonstop=include_nonstop)