]> git.0d.be Git - panikweb.git/blob - panikweb/views.py
prepare support for multiple players
[panikweb.git] / panikweb / views.py
1 import math
2 import os
3 import random
4 import stat
5 import urllib.parse
6 from datetime import date, datetime, time, timedelta
7
8 import pkg_resources
9 from combo.data.models import Page
10 from combo.public.views import publish_page
11 from django.conf import settings
12 from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
13 from django.contrib.sites.shortcuts import get_current_site
14 from django.contrib.syndication.views import Feed, add_domain
15 from django.core.files.storage import default_storage
16 from django.core.paginator import Paginator
17 from django.http import Http404, HttpResponse, JsonResponse
18 from django.urls import reverse
19 from django.utils.encoding import force_str
20 from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
21 from django.views.decorators.cache import cache_control
22 from django.views.decorators.csrf import csrf_exempt
23 from django.views.generic import RedirectView, View
24 from django.views.generic.base import TemplateView
25 from django.views.generic.dates import MonthArchiveView, _date_from_string
26 from django.views.generic.detail import DetailView
27 from emissions.app_settings import app_settings as emissions_app_settings
28 from emissions.models import (
29     Category,
30     Diffusion,
31     Emission,
32     Episode,
33     Focus,
34     NewsCategory,
35     NewsItem,
36     Nonstop,
37     Schedule,
38     SoundFile,
39 )
40 from emissions.utils import period_program, whatsonair
41 from emissions.views import EmissionEpisodeMixin
42 from haystack.query import SearchQuerySet
43 from newsletter.forms import SubscribeForm
44 from nonstop.models import SomaLogLine
45 from nonstop.utils import get_current_nonstop_track
46 from panikombo.models import ItemTopik
47 from sorl.thumbnail.shortcuts import get_thumbnail
48
49 from . import utils
50
51
52 class EmissionMixin:
53     def get_emission_context(self, emission, episode_ids=None):
54         context = {}
55
56         # get all episodes, with an additional attribute to get the date of
57         # their first diffusion
58         episodes_queryset = Episode.objects.select_related()
59         if episode_ids is not None:
60             episodes_queryset = episodes_queryset.filter(id__in=episode_ids)
61         else:
62             episodes_queryset = episodes_queryset.filter(emission=emission)
63
64         if settings.USE_AGENDA_ONLY_FIELD:
65             episodes_queryset = episodes_queryset.exclude(agenda_only=True)
66
67         context['episodes'] = (
68             episodes_queryset.extra(
69                 select={
70                     'first_diffusion': 'emissions_diffusion.datetime',
71                 },
72                 select_params=(False, True),
73                 where=[
74                     '''datetime = (SELECT MIN(datetime)
75                                                 FROM emissions_diffusion
76                                                WHERE episode_id = emissions_episode.id
77                                                  AND datetime <= CURRENT_TIMESTAMP)'''
78                 ],
79                 tables=['emissions_diffusion'],
80             )
81             .order_by('-first_diffusion')
82             .distinct()
83         )
84
85         context['all_episodes'] = (
86             episodes_queryset.extra(
87                 select={
88                     'first_diffusion': 'emissions_diffusion.datetime',
89                 },
90                 select_params=(False, True),
91                 where=[
92                     '''datetime = (SELECT MIN(datetime)
93                                                 FROM emissions_diffusion
94                                                WHERE episode_id = emissions_episode.id)'''
95                 ],
96                 tables=['emissions_diffusion'],
97             )
98             .order_by('-first_diffusion')
99             .distinct()
100         )
101
102         context['futurEpisodes'] = (
103             episodes_queryset.extra(
104                 select={
105                     'first_diffusion': 'emissions_diffusion.datetime',
106                 },
107                 select_params=(False, True),
108                 where=[
109                     '''datetime = (SELECT MIN(datetime)
110                                                 FROM emissions_diffusion
111                                                WHERE episode_id = emissions_episode.id
112                                                  AND datetime > CURRENT_TIMESTAMP)'''
113                 ],
114                 tables=['emissions_diffusion'],
115             )
116             .order_by('first_diffusion')
117             .distinct()
118         )
119
120         # get all related soundfiles in a single query
121         soundfiles = {}
122         if episode_ids is not None:
123             for episode_id in episode_ids:
124                 soundfiles[episode_id] = None
125         else:
126             for episode in Episode.objects.filter(emission=emission):
127                 soundfiles[episode.id] = None
128
129         for soundfile in SoundFile.objects.select_related().filter(
130             podcastable=True, fragment=False, episode__emission=emission
131         ):
132             soundfiles[soundfile.episode_id] = soundfile
133
134         Episode.set_prefetched_soundfiles(soundfiles)
135
136         # context['futurEpisodes'] = context['episodes'].filter(first_diffusion='2013')[0:3]
137
138         return context
139
140
141 class EmissionDetailView(DetailView, EmissionMixin):
142     model = Emission
143
144     def get_context_data(self, **kwargs):
145         context = super().get_context_data(**kwargs)
146         context['schedules'] = (
147             Schedule.objects.select_related().filter(emission=self.object).order_by('rerun', 'datetime')
148         )
149         context['news'] = (
150             NewsItem.objects.all()
151             .filter(emission=self.object.id)
152             .exclude(expiration_date__lt=date.today())  # expiration date
153             .exclude(date__lt=date.today() - timedelta(days=60))
154             .order_by('-date')[:3]
155         )
156         try:
157             nonstop_object = Nonstop.objects.get(slug=self.object.slug)
158         except Nonstop.DoesNotExist:
159             pass
160         else:
161             today = date.today()
162             dates = [today - timedelta(days=x) for x in range(7)]
163             if datetime.now().time() < nonstop_object.start:
164                 dates = dates[1:]
165             context['nonstop'] = nonstop_object
166             context['nonstop_dates'] = dates
167         context.update(self.get_emission_context(self.object))
168         return context
169
170
171 emission = EmissionDetailView.as_view()
172
173
174 class EpisodeDetailView(EmissionEpisodeMixin, DetailView, EmissionMixin):
175     model = Episode
176
177     def get_context_data(self, **kwargs):
178         context = super().get_context_data(**kwargs)
179         context['diffusions'] = (
180             Diffusion.objects.select_related().filter(episode=self.object.id).order_by('datetime')
181         )
182         try:
183             context['emission'] = context['episode'].emission
184         except Emission.DoesNotExist:
185             raise Http404()
186         if self.kwargs.get('emission_slug') != context['emission'].slug:
187             raise Http404()
188         context.update(self.get_emission_context(context['emission']))
189         context['topik_pages'] = [x.page for x in ItemTopik.objects.filter(episode=self.object)]
190         return context
191
192
193 episode = EpisodeDetailView.as_view()
194
195
196 class NonstopPlaylistView(TemplateView):
197     template_name = 'nonstop_playlist.html'
198
199     def get_context_data(self, **kwargs):
200         context = super().get_context_data(**kwargs)
201         try:
202             context['date'] = date(int(kwargs.get('year')), int(kwargs.get('month')), int(kwargs.get('day')))
203         except ValueError:
204             raise Http404()
205         context['future'] = context['date'] >= date.today()
206
207         context['emission'] = Emission.objects.filter(slug=kwargs.get('slug')).first()
208         try:
209             nonstop_object = Nonstop.objects.get(slug=kwargs.get('slug'))
210         except Nonstop.DoesNotExist:
211             raise Http404()
212         context['nonstop'] = nonstop_object
213         start = datetime(
214             int(kwargs.get('year')),
215             int(kwargs.get('month')),
216             int(kwargs.get('day')),
217             nonstop_object.start.hour,
218             nonstop_object.start.minute,
219         )
220         end = datetime(
221             int(kwargs.get('year')),
222             int(kwargs.get('month')),
223             int(kwargs.get('day')),
224             nonstop_object.end.hour,
225             nonstop_object.end.minute,
226         )
227         if end < start:
228             end = end + timedelta(days=1)
229         context['tracks'] = (
230             SomaLogLine.objects.filter(play_timestamp__gte=start, play_timestamp__lte=end)
231             .exclude(on_air=False)
232             .select_related()
233         )
234         return context
235
236
237 nonstop_playlist = NonstopPlaylistView.as_view()
238
239
240 class EmissionEpisodesDetailView(DetailView, EmissionMixin):
241     model = Emission
242     template_name = 'emissions/episodes.html'
243
244     def get_context_data(self, **kwargs):
245         context = super().get_context_data(**kwargs)
246         context['schedules'] = (
247             Schedule.objects.select_related().filter(emission=self.object).order_by('rerun', 'datetime')
248         )
249         count_per_month = 0
250         for schedule in context['schedules']:
251             if schedule.rerun:
252                 continue
253             count_per_month += bin(schedule.weeks).count('1')
254
255         context['count_per_month'] = count_per_month
256
257         context['search_query'] = self.request.GET.get('q')
258         if context['search_query']:
259             if settings.USE_HAYSTACK:
260                 # query string
261                 sqs = (
262                     SearchQuerySet()
263                     .models(Episode)
264                     .filter(emission_slug_exact=self.object.slug, text=context['search_query'])
265                 )
266                 episode_ids = [x.pk for x in sqs]
267             else:
268                 vector = SearchVector(
269                     'title', config=settings.FTS_DICTIONARY_CONFIG, weight='A'
270                 ) + SearchVector('text', config=settings.FTS_DICTIONARY_CONFIG, weight='B')
271                 query = SearchQuery(context['search_query'], config=settings.FTS_DICTIONARY_CONFIG)
272                 qs = Episode.objects.filter(emission=self.object)
273                 qs = qs.annotate(rank=SearchRank(vector, query)).filter(rank__gte=0.1).order_by('-rank')
274                 episode_ids = qs.values_list('id', flat=True)
275         else:
276             episode_ids = None
277
278         context.update(self.get_emission_context(self.object, episode_ids=episode_ids))
279         return context
280
281
282 emissionEpisodes = EmissionEpisodesDetailView.as_view()
283
284
285 class SoundFileEmbedView(DetailView):
286     model = SoundFile
287     template_name = 'soundfiles/embed.html'
288
289     def get_context_data(self, **kwargs):
290         context = super().get_context_data(**kwargs)
291         if self.kwargs.get('episode_slug') != self.object.episode.slug:
292             raise Http404()
293         if self.kwargs.get('emission_slug') != self.object.episode.emission.slug:
294             raise Http404()
295         context['episode'] = self.object.episode
296         return context
297
298
299 soundfile_embed = SoundFileEmbedView.as_view()
300
301
302 class EpisodeEmbedRedirect(RedirectView):
303     def get_redirect_url(self, **kwargs):
304         try:
305             soundfile = SoundFile.objects.get(
306                 episode__slug=kwargs['episode_slug'],
307                 episode__emission__slug=kwargs['emission_slug'],
308                 fragment=False,
309                 podcastable=True,
310             )
311         except SoundFile.DoesNotExist:
312             raise Http404()
313         kwargs['pk'] = soundfile.id
314         return reverse('soundfile-embed-view', kwargs=kwargs)
315
316
317 episode_embed_redirect = EpisodeEmbedRedirect.as_view()
318
319
320 class SoundFileDialogEmbedView(DetailView):
321     model = SoundFile
322     template_name = 'soundfiles/dialog-embed.html'
323
324     def get_context_data(self, **kwargs):
325         context = super().get_context_data(**kwargs)
326         if self.kwargs.get('episode_slug') != self.object.episode.slug:
327             raise Http404()
328         if self.kwargs.get('emission_slug') != self.object.episode.emission.slug:
329             raise Http404()
330         context['episode'] = self.object.episode
331         return context
332
333
334 soundfile_dlg_embed = SoundFileDialogEmbedView.as_view()
335
336
337 class ProgramView(TemplateView):
338     template_name = 'program.html'
339
340     def get_context_data(self, year=None, week=None, **kwargs):
341         context = super().get_context_data(**kwargs)
342
343         context['weekday'] = datetime.today().weekday()
344
345         context['week'] = week = int(week) if week is not None else datetime.today().isocalendar()[1]
346         context['year'] = year = int(year) if year is not None else datetime.today().isocalendar()[0]
347         if context['week'] > 53:
348             raise Http404()
349         context['week_first_day'] = utils.tofirstdayinisoweek(year, week)
350         context['week_last_day'] = context['week_first_day'] + timedelta(days=6)
351
352         return context
353
354
355 program = ProgramView.as_view()
356
357
358 class TimeCell:
359     nonstop = None
360     w = 1
361     h = 1
362     time_label = None
363
364     def __init__(self, i, j):
365         self.x = i
366         self.y = j
367         self.schedules = []
368
369     def add_schedule(self, schedule):
370         same_emission_and_duration = [
371             x
372             for x in self.schedules
373             if (x.emission_id == schedule.emission_id and x.get_duration() == schedule.get_duration())
374         ]
375         if same_emission_and_duration:
376             # add extra week/s to existing schedule
377             same_emission_and_duration[0].weeks |= schedule.weeks
378             return
379         end_time = schedule.datetime + timedelta(minutes=schedule.get_duration())
380         self.time_label = '%02d:%02d-%02d:%02d' % (
381             schedule.datetime.hour,
382             schedule.datetime.minute,
383             end_time.hour,
384             end_time.minute,
385         )
386         self.schedules.append(schedule)
387
388     def sorted_schedules(self):
389         return sorted(self.schedules, key=lambda x: x.week_sort_key())
390
391     def __str__(self):
392         if self.schedules:
393             return ', '.join([x.emission.title for x in self.schedules])
394         else:
395             return self.nonstop
396
397     def __eq__(self, other):
398         return force_str(self) == force_str(other) and self.time_label == other.time_label
399
400
401 class Grid(TemplateView):
402     template_name = 'grid.html'
403
404     def get_context_data(self, **kwargs):
405         context = super().get_context_data(**kwargs)
406
407         nb_lines = 2 * 24  # the cells are half hours
408         grid = []
409
410         times = ['%02d:%02d' % (x / 2, x % 2 * 30) for x in range(nb_lines)]
411         # start grid after the night programs
412         times = (
413             times[
414                 2 * emissions_app_settings.DAY_HOUR_START
415                 + (1 if emissions_app_settings.DAY_MINUTE_START else 0) :
416             ]
417             + times[
418                 : 2 * emissions_app_settings.DAY_HOUR_START
419                 + (1 if emissions_app_settings.DAY_MINUTE_START else 0)
420             ]
421         )
422
423         nonstops = []
424         for nonstop in Nonstop.objects.all():
425             if nonstop.start == nonstop.end:
426                 continue
427             if nonstop.start < nonstop.end:
428                 nonstops.append(
429                     [
430                         nonstop.start.hour + nonstop.start.minute / 60.0,
431                         nonstop.end.hour + nonstop.end.minute / 60.0,
432                         nonstop.get_public_label(),
433                         nonstop.slug,
434                         nonstop,
435                     ]
436                 )
437             else:
438                 # crossing midnight
439                 nonstops.append(
440                     [
441                         nonstop.start.hour + nonstop.start.minute / 60.0,
442                         24,
443                         nonstop.get_public_label(),
444                         nonstop.slug,
445                         nonstop,
446                     ]
447                 )
448                 nonstops.append(
449                     [
450                         0,
451                         nonstop.end.hour + nonstop.end.minute / 60.0,
452                         nonstop.get_public_label(),
453                         nonstop.slug,
454                         nonstop,
455                     ]
456                 )
457         nonstops.sort()
458
459         for i in range(nb_lines):
460             grid.append([])
461             for j in range(7):
462                 grid[-1].append(TimeCell(i, j))
463
464             try:
465                 nonstop = [x for x in nonstops if i >= x[0] * 2 and i < x[1] * 2][0]
466             except IndexError:
467                 nonstop = [0, 24, '', '', None]
468             for time_cell in grid[-1]:
469                 time_cell.nonstop = nonstop[2]
470                 time_cell.nonstop_slug = nonstop[3]
471                 time_cell.redirect_path = nonstop[4].redirect_path if nonstop[4] else None
472                 if (
473                     nonstop[1]
474                     == emissions_app_settings.DAY_HOUR_START + emissions_app_settings.DAY_MINUTE_START / 60
475                 ):
476                     # the one ending at dawn will be cut down, so we inscribe
477                     # its duration manually
478                     time_cell.time_label = '%02d:00-%02d:%02d' % (
479                         nonstop[0],
480                         nonstop[1],
481                         emissions_app_settings.DAY_MINUTE_START,
482                     )
483
484         for schedule in (
485             Schedule.objects.prefetch_related('emission__categories').select_related().order_by('datetime')
486         ):
487             row_start = schedule.datetime.hour * 2 + int(math.ceil(schedule.datetime.minute / 30))
488             if schedule.get_duration() < 30:
489                 # special case for an emission during 12:45-13:00
490                 row_start = schedule.datetime.hour * 2 + int(math.floor(schedule.datetime.minute / 30))
491             day_no = schedule.get_weekday()
492
493             for step in range(int(math.ceil(schedule.get_duration() / 30.0))):
494                 if grid[(row_start + step) % nb_lines][day_no] is None:
495                     grid[(row_start + step) % nb_lines][day_no] = TimeCell()
496                 grid[(row_start + step) % nb_lines][day_no].add_schedule(schedule)
497
498         # start grid after the night programs
499         grid = (
500             grid[
501                 2 * emissions_app_settings.DAY_HOUR_START
502                 + (1 if emissions_app_settings.DAY_MINUTE_START else 0) :
503             ]
504             + grid[
505                 : 2 * emissions_app_settings.DAY_HOUR_START
506                 + (1 if emissions_app_settings.DAY_MINUTE_START else 0)
507             ]
508         )
509
510         # look for the case where the same emission has different schedules for
511         # the same time cell, for example if it lasts one hour the first week,
512         # and two hours the third week.
513         for i in range(nb_lines):
514             grid[i] = [x for x in grid[i] if x is not None]
515             for j, cell in enumerate(grid[i]):
516                 if grid[i][j] is None:
517                     continue
518                 if len(grid[i][j].schedules) > 1:
519                     time_cell_emissions = {}
520                     for schedule in grid[i][j].schedules:
521                         if not schedule.emission.id in time_cell_emissions:
522                             time_cell_emissions[schedule.emission.id] = []
523                         time_cell_emissions[schedule.emission.id].append(schedule)
524                     for schedule_list in time_cell_emissions.values():
525                         if len(schedule_list) == 1:
526                             continue
527                         # here it is, same cell, same emission, several
528                         # schedules
529                         schedule_list.sort(key=lambda x: x.get_duration())
530
531                         schedule = schedule_list[0]
532                         end_time = schedule.datetime + timedelta(minutes=schedule.get_duration())
533                         grid[i][j].time_label = '%02d:%02d-%02d:%02d' % (
534                             schedule.datetime.hour,
535                             schedule.datetime.minute,
536                             end_time.hour,
537                             end_time.minute,
538                         )
539
540                         schedule_list.sort(key=lambda x: x.weeks)
541                         for schedule in schedule_list[1:]:
542                             grid[i][j].schedules.remove(schedule)
543                             end_time = schedule.datetime + timedelta(minutes=schedule.get_duration())
544                             if schedule_list[0].get_duration() == schedule.get_duration():
545                                 # same duration, append week info
546                                 schedule_list[0].time_label_extra = ', %s' % (schedule.weeks_string,)
547                             else:
548                                 # different durations, also append other
549                                 # endtime info
550                                 schedule_list[0].time_label_extra = ', -%02d:%02d %s' % (
551                                     end_time.hour,
552                                     end_time.minute,
553                                     schedule.weeks_string,
554                                 )
555                         pass
556
557         # merge adjacent
558
559         # 1st thing is to merge cells on the same line, this will mostly catch
560         # consecutive nonstop cells
561         for i in range(nb_lines):
562             for j, cell in enumerate(grid[i]):
563                 if grid[i][j] is None:
564                     continue
565                 t = 1
566                 try:
567                     # if the cells are identical, they are removed from the
568                     # grid, and current cell width is increased
569                     while grid[i][j + t] == cell:
570                         cell.w += 1
571                         grid[i][j + t] = None
572                         t += 1
573                 except IndexError:
574                     pass
575
576             # once we're done we remove empty cells
577             grid[i] = [x for x in grid[i] if x is not None]
578
579         # 2nd thing is to merge cells vertically, this is emissions that last
580         # for more than 30 minutes
581         for i in range(nb_lines):
582             grid[i] = [x for x in grid[i] if x is not None]
583             for j, cell in enumerate(grid[i]):
584                 if grid[i][j] is None:
585                     continue
586                 t = 1
587                 try:
588                     while True:
589                         # we look if the next time cell has the same emissions
590                         same_cell_below = [
591                             (bj, x)
592                             for bj, x in enumerate(grid[i + cell.h])
593                             if x == cell and x.y == cell.y and x.w == cell.w
594                         ]
595                         if same_cell_below:
596                             # if the cell was identical, we remove it and
597                             # increase current cell height
598                             bj, same_cell_below = same_cell_below[0]
599                             del grid[i + cell.h][bj]
600                             cell.h += 1
601                         else:
602                             # if the cell is different, we have a closer look
603                             # to it, so we can remove emissions that will
604                             # already be mentioned in the current cell.
605                             #
606                             # For example:
607                             #  - 7am30, seuls contre tout, 1h30
608                             #  - 8am, du pied gauche & la voix de la rue, 1h
609                             # should produce: (this is case A)
610                             #  |      7:30-9:00      |
611                             #  |  seuls contre tout  |
612                             #  |---------------------|
613                             #  |      8:00-9:00      |
614                             #  |   du pied gauche    |
615                             #  |  la voix de la rue  |
616                             #
617                             # On the other hand, if all three emissions started
618                             # at 7am30, we want: (this is case B)
619                             #  |      7:30-9:00      |
620                             #  |  seuls contre tout  |
621                             #  |   du pied gauche    |
622                             #  |  la voix de la rue  |
623                             # that is we merge all of them, ignoring the fact
624                             # that the other emissions will stop at 8am30
625                             current_cell_schedules = set(grid[i][j].schedules)
626                             current_cell_emissions = {x.emission for x in current_cell_schedules}
627                             cursor = 1
628                             while True and current_cell_schedules:
629                                 same_cell_below = [x for x in grid[i + cursor] if x.y == grid[i][j].y]
630                                 if not same_cell_below:
631                                     cursor += 1
632                                     continue
633                                 same_cell_below = same_cell_below[0]
634                                 same_cell_below_emissions = {x.emission for x in same_cell_below.schedules}
635
636                                 if current_cell_emissions.issubset(same_cell_below_emissions):
637                                     # this handles case A (see comment above)
638                                     for schedule in current_cell_schedules:
639                                         if schedule in same_cell_below.schedules:
640                                             same_cell_below.schedules.remove(schedule)
641                                 elif same_cell_below_emissions and current_cell_emissions.issuperset(
642                                     same_cell_below_emissions
643                                 ):
644                                     # this handles case B (see comment above)
645                                     # we set the cell time label to the longest
646                                     # period
647                                     grid[i][j].time_label = same_cell_below.time_label
648                                     # then we sort emissions so the longest are
649                                     # put first
650                                     grid[i][j].schedules.sort(key=lambda x: -x.get_duration())
651                                     # then we add individual time labels to the
652                                     # other schedules
653                                     for schedule in current_cell_schedules:
654                                         if schedule not in same_cell_below.schedules:
655                                             end_time = schedule.datetime + timedelta(
656                                                 minutes=schedule.get_duration()
657                                             )
658                                             schedule.time_label = '%02d:%02d-%02d:%02d' % (
659                                                 schedule.datetime.hour,
660                                                 schedule.datetime.minute,
661                                                 end_time.hour,
662                                                 end_time.minute,
663                                             )
664                                     grid[i][j].h += 1
665                                     grid[i + cursor].remove(same_cell_below)
666                                 elif same_cell_below_emissions and current_cell_emissions.intersection(
667                                     same_cell_below_emissions
668                                 ):
669                                     same_cell_below.schedules = [
670                                         x
671                                         for x in same_cell_below.schedules
672                                         if x.emission not in current_cell_emissions or x.get_duration() < 30
673                                     ]
674                                 cursor += 1
675                             break
676                 except IndexError:
677                     pass
678
679         # cut late night hours
680         grid = grid[:44]
681         times = times[:44]
682
683         context['grid'] = grid
684         context['times'] = times
685         context['categories'] = Category.objects.all()
686         # dates from Monday to Sunday
687         context['weekdays'] = [date(2018, 1, x) for x in range(1, 8)]
688
689         return context
690
691
692 grid = Grid.as_view()
693
694
695 class Home(TemplateView):
696     template_name = 'home.html'
697
698     def dispatch(self, request, *args, **kwargs):
699         page = Page.objects.filter(slug='index', parent__isnull=True).first()
700         if page:
701             return publish_page(request, page)
702         return super().dispatch(request, *args, **kwargs)
703
704     def get_context_data(self, **kwargs):
705         context = super().get_context_data(**kwargs)
706         context['emissions'] = Emission.objects.filter(archived=False).order_by('-creation_timestamp')[
707             : settings.HOME_EMISSIONS_COUNT
708         ]
709         context['newsitems'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')[
710             : settings.HOME_NEWSITEMS_COUNT
711         ]
712
713         context['soundfiles'] = (
714             SoundFile.objects.prefetch_related('episode__emission__categories')
715             .filter(podcastable=True, fragment=False)
716             .filter(
717                 creation_timestamp__lt=datetime.now() - timedelta(minutes=settings.PODCASTS_PUBLICATION_DELAY)
718             )
719             .select_related()
720             .extra(
721                 select={
722                     'first_diffusion': 'emissions_diffusion.datetime',
723                 },
724                 select_params=(False, True),
725                 where=[
726                     '''datetime = (SELECT MIN(datetime)
727                                             FROM emissions_diffusion
728                                         WHERE episode_id = emissions_episode.id)'''
729                 ],
730                 tables=['emissions_diffusion'],
731             )
732             .order_by('-creation_timestamp')
733             .distinct()[: settings.HOME_PODCASTS_COUNT]
734         )
735
736         context['newsletter_form'] = SubscribeForm()
737
738         return context
739
740
741 home = Home.as_view()
742
743
744 class NewsItemView(DetailView):
745     model = NewsItem
746
747     def get_context_data(self, **kwargs):
748         context = super().get_context_data(**kwargs)
749         context['categories'] = NewsCategory.objects.all()
750         context['news'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')
751         context['topik_pages'] = [x.page for x in ItemTopik.objects.filter(newsitem=self.object)]
752         return context
753
754
755 newsitemview = NewsItemView.as_view()
756
757
758 class News(TemplateView):
759     template_name = 'news.html'
760
761     def get_context_data(self, **kwargs):
762         context = super().get_context_data(**kwargs)
763         context['focus'] = (
764             NewsItem.objects.exclude(date__gt=date.today())  # publication date
765             .exclude(expiration_date__lt=date.today())  # expiration date
766             .filter(got_focus__isnull=False)
767             .select_related('category')
768             .order_by('-date')[:10]
769         )
770         context['news'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')
771         context['news_not_expired'] = (
772             NewsItem.objects.exclude(date__gt=date.today())
773             .exclude(expiration_date__lt=date.today())
774             .order_by('-date')
775         )
776         return context
777
778
779 news = News.as_view()
780
781
782 class Agenda(TemplateView):
783     template_name = 'agenda.html'
784
785     def get_context_data(self, **kwargs):
786         context = super().get_context_data(**kwargs)
787         context['agenda'] = (
788             NewsItem.objects.exclude(date__gt=date.today())
789             .filter(event_date__gte=date.today())
790             .order_by('event_date')[:20]
791         )
792         context['news'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')
793         context['previous_month'] = datetime.today().replace(day=1) - timedelta(days=2)
794         return context
795
796
797 agenda = Agenda.as_view()
798
799
800 class AgendaByMonth(MonthArchiveView):
801     template_name = 'agenda.html'
802     queryset = NewsItem.objects.filter(event_date__isnull=False)
803     allow_future = True
804     date_field = 'event_date'
805     month_format = '%m'
806
807     def get_context_data(self, **kwargs):
808         context = super().get_context_data(**kwargs)
809         context['agenda'] = context['object_list']
810         context['news'] = NewsItem.objects.all().order_by('-date')
811         return context
812
813
814 agenda_by_month = AgendaByMonth.as_view()
815
816
817 class Emissions(TemplateView):
818     template_name = 'emissions.html'
819
820     def get_queryset(self):
821         return Emission.objects.prefetch_related('categories').filter(archived=False).order_by('title')
822
823     def get_context_data(self, **kwargs):
824         context = super().get_context_data(**kwargs)
825         context['emissions'] = self.get_queryset()
826         context['categories'] = Category.objects.all()
827         return context
828
829
830 emissions = Emissions.as_view()
831
832
833 class EmissionsArchives(TemplateView):
834     template_name = 'emissions/archives.html'
835
836     def get_context_data(self, **kwargs):
837         context = super().get_context_data(**kwargs)
838         context['emissions'] = (
839             Emission.objects.prefetch_related('categories').filter(archived=True).order_by('title')
840         )
841         context['categories'] = Category.objects.all()
842         return context
843
844
845 emissionsArchives = EmissionsArchives.as_view()
846
847
848 class Listen(TemplateView):
849     template_name = 'listen.html'
850
851     def get_context_data(self, **kwargs):
852         context = super().get_context_data(**kwargs)
853         context['focus'] = (
854             SoundFile.objects.prefetch_related('episode__emission__categories')
855             .filter(podcastable=True, got_focus__isnull=False)
856             .select_related()
857             .extra(
858                 select={
859                     'first_diffusion': 'emissions_diffusion.datetime',
860                 },
861                 select_params=(False, True),
862                 where=[
863                     '''datetime = (SELECT MIN(datetime)
864                                             FROM emissions_diffusion
865                                         WHERE episode_id = emissions_episode.id)'''
866                 ],
867                 tables=['emissions_diffusion'],
868             )
869             .order_by('-first_diffusion')
870             .distinct()[:10]
871         )
872         context['soundfiles'] = (
873             SoundFile.objects.prefetch_related('episode__emission__categories')
874             .filter(podcastable=True)
875             .select_related()
876             .extra(
877                 select={
878                     'first_diffusion': 'emissions_diffusion.datetime',
879                 },
880                 select_params=(False, True),
881                 where=[
882                     '''datetime = (SELECT MIN(datetime)
883                                             FROM emissions_diffusion
884                                         WHERE episode_id = emissions_episode.id)'''
885                 ],
886                 tables=['emissions_diffusion'],
887             )
888             .order_by('-creation_timestamp')
889             .distinct()[:20]
890         )
891
892         return context
893
894
895 listen = Listen.as_view()
896
897
898 class OnAir(View):
899     def get_infos(self, ctx):
900         infos = {}
901         include_track_metadata = settings.ONAIR_ALWAYS_INCLUDE_TRACK_METADATA
902
903         if ctx.get('episode'):
904             infos['episode'] = {
905                 'title': ctx['episode'].title,
906                 'subtitle': ctx['episode'].subtitle,
907                 'url': ctx['episode'].get_absolute_url(),
908             }
909
910         if ctx.get('emission'):
911             chat_url = None
912             if ctx['emission'].chat_open:
913                 chat_url = reverse('emission-chat', kwargs={'slug': ctx['emission'].slug})
914             infos['emission'] = {
915                 'title': ctx['emission'].title,
916                 'subtitle': ctx['emission'].subtitle,
917                 'slug': ctx['emission'].slug,
918                 'url': ctx['emission'].get_absolute_url(),
919                 'chat': chat_url,
920             }
921             if (
922                 hasattr(ctx['current_slot'], 'recurringplaylistdiffusion_set')
923                 and ctx['current_slot'].recurringplaylistdiffusion_set.exists()
924             ):
925                 include_track_metadata = True
926
927         if ctx.get('nonstop'):
928             include_track_metadata = True
929             redirect_path = ctx['nonstop'].redirect_path
930             infos['nonstop'] = {
931                 'title': ctx['nonstop'].get_public_label(),
932                 'slug': ctx['current_slot'].slug,
933             }
934             if redirect_path:
935                 infos['nonstop']['url'] = redirect_path
936             today = datetime.today()
937             infos['nonstop']['playlist_url'] = reverse(
938                 'nonstop-playlist',
939                 kwargs={
940                     'year': today.year,
941                     'month': today.month,
942                     'day': today.day,
943                     'slug': ctx['current_slot'].slug,
944                 },
945             )
946
947         if include_track_metadata:
948             infos.update(get_current_nonstop_track())
949
950         return infos
951
952     def get(self, request, *args, **kwargs):
953         infos = self.get_infos(ctx=whatsonair())
954         return JsonResponse({'data': infos})
955
956
957 onair = cache_control(max_age=15)(csrf_exempt(OnAir.as_view()))
958
959
960 class DabService(View):
961     def get_infos(self, ctx):
962         infos = {'text1': '', 'text2': '', 'text3': ''}
963         include_track_metadata = settings.ONAIR_ALWAYS_INCLUDE_TRACK_METADATA
964
965         if ctx.get('episode'):
966             infos['text1'] = ctx['episode'].title
967             infos['text2'] = ctx['emission'].title
968         elif ctx.get('emission'):
969             infos['text1'] = ctx['emission'].title
970             if (
971                 hasattr(ctx['current_slot'], 'recurringplaylistdiffusion_set')
972                 and ctx['current_slot'].recurringplaylistdiffusion_set.exists()
973             ):
974                 include_track_metadata = True
975         elif ctx.get('nonstop'):
976             include_track_metadata = True
977
978         if include_track_metadata:
979             track_info = get_current_nonstop_track()
980             if track_info.get('track_artist'):
981                 infos['text1'] = track_info['track_artist']
982                 infos['text2'] = track_info['track_title']
983             elif track_info.get('track_title'):
984                 infos['text1'] = track_info['track_title']
985             elif ctx.get('nonstop'):
986                 infos['text1'] = ctx.get('nonstop').title
987
988         return infos
989
990     def get(self, request, *args, **kwargs):
991         infos = self.get_infos(ctx=whatsonair())
992         return JsonResponse({'data': infos})
993
994
995 dab_service = cache_control(max_age=15)(csrf_exempt(DabService.as_view()))
996
997
998 class NewsItemDetailView(DetailView):
999     model = NewsItem
1000
1001
1002 newsitem = NewsItemDetailView.as_view()
1003
1004
1005 class RssCustomPodcastsFeed(Rss201rev2Feed):
1006     def add_root_elements(self, handler):
1007         super().add_root_elements(handler)
1008         emission = self.feed.get('emission')
1009         if emission and emission.image and emission.image.url:
1010             if settings.PODCAST_IMAGE_GEOMETRY:
1011                 image_url = get_thumbnail(
1012                     emission.image,
1013                     settings.PODCAST_IMAGE_GEOMETRY,
1014                     **settings.PODCAST_IMAGE_THUMBNAIL_OPTIONS,
1015                 ).url
1016             else:
1017                 image_url = emission.image.url
1018         else:
1019             image_url = settings.PODCASTS_DEFAULT_IMAGE_PATH
1020         image_url = urllib.parse.urljoin(self.feed['link'], image_url)
1021         handler.startElement('image', {})
1022         if emission:
1023             handler.addQuickElement('title', emission.title)
1024         else:
1025             handler.addQuickElement('title', settings.RADIO_NAME)
1026         handler.addQuickElement('url', image_url)
1027         handler.endElement('image')
1028         handler.addQuickElement('itunes:explicit', 'no')  # invidividual items will get their own value
1029         handler.addQuickElement('itunes:image', None, {'href': image_url})
1030         if emission:
1031             if emission.subtitle:
1032                 handler.addQuickElement('itunes:subtitle', emission.subtitle)
1033             for category in emission.categories.all():
1034                 if category.itunes_category:
1035                     handler.addQuickElement('itunes:category', None, {'text': category.itunes_category})
1036
1037         handler.addQuickElement('itunes:author', settings.RADIO_NAME)
1038         handler.startElement('itunes:owner', {})
1039         if emission and emission.email:
1040             handler.addQuickElement('itunes:email', emission.email)
1041         else:
1042             handler.addQuickElement('itunes:email', settings.DEFAULT_FROM_EMAIL)
1043         handler.addQuickElement('itunes:name', settings.RADIO_NAME)
1044         handler.endElement('itunes:owner')
1045
1046     def root_attributes(self):
1047         attrs = super().root_attributes()
1048         attrs['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/'
1049         attrs['xmlns:itunes'] = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
1050         return attrs
1051
1052     def add_item_elements(self, handler, item):
1053         super().add_item_elements(handler, item)
1054         explicit = 'no'
1055         for tag in item.get('tags') or []:
1056             handler.addQuickElement('dc:subject', tag)
1057             if tag == 'explicit':
1058                 explicit = 'yes'
1059         if item.get('tags'):
1060             handler.addQuickElement('itunes:keywords', ','.join(item.get('tags')))
1061         handler.addQuickElement('itunes:explicit', explicit)
1062         episode = item.get('episode')
1063         if episode and episode.image and episode.image.url:
1064             if settings.PODCAST_IMAGE_GEOMETRY:
1065                 image_url = get_thumbnail(
1066                     episode.image, settings.PODCAST_IMAGE_GEOMETRY, **settings.PODCAST_IMAGE_THUMBNAIL_OPTIONS
1067                 ).url
1068             else:
1069                 image_url = episode.image.url
1070             image_url = urllib.parse.urljoin(self.feed['link'], image_url)
1071             handler.addQuickElement('itunes:image', None, {'href': image_url})
1072         soundfile = item.get('soundfile')
1073         if soundfile.duration:
1074             handler.addQuickElement(
1075                 'itunes:duration',
1076                 '%02d:%02d:%02d'
1077                 % (soundfile.duration / 3600, soundfile.duration % 3600 / 60, soundfile.duration % 60),
1078             )
1079
1080
1081 class PodcastsFeed(Feed):
1082     title = '%s - Podcasts' % settings.RADIO_NAME
1083     link = '/'
1084     description_template = 'feed/soundfile.html'
1085     feed_type = RssCustomPodcastsFeed
1086
1087     def get_feed(self, obj, request):
1088         self.request = request
1089         return super().get_feed(obj, request)
1090
1091     @property
1092     def description(self):
1093         return settings.RADIO_META_DESCRIPTION
1094
1095     def items(self):
1096         return (
1097             SoundFile.objects.select_related()
1098             .filter(podcastable=True)
1099             .filter(
1100                 creation_timestamp__lt=datetime.now() - timedelta(minutes=settings.PODCASTS_PUBLICATION_DELAY)
1101             )
1102             .exclude(file__isnull=True)
1103             .exclude(file='')
1104             .order_by('-creation_timestamp')[:50]
1105         )
1106
1107     def item_title(self, item):
1108         if item.fragment:
1109             return '[%s] %s - %s' % (item.episode.emission.title, item.title, item.episode.title)
1110         return '[%s] %s' % (item.episode.emission.title, item.episode.title)
1111
1112     def item_link(self, item):
1113         if item.fragment:
1114             return item.episode.get_absolute_url() + '#%s' % item.id
1115         return item.episode.get_absolute_url()
1116
1117     def item_enclosure_url(self, item):
1118         current_site = get_current_site(request=self.request)
1119         return add_domain(current_site.domain, item.get_format_url('mp3'), self.request.is_secure())
1120
1121     def item_enclosure_length(self, item):
1122         if item.mp3_file_size:
1123             return item.mp3_file_size
1124         sound_path = item.get_format_path('mp3')
1125         try:
1126             return os.stat(sound_path)[stat.ST_SIZE]
1127         except OSError:
1128             return 0
1129
1130     def item_enclosure_mime_type(self, item):
1131         return 'audio/mpeg'
1132
1133     def item_pubdate(self, item):
1134         return item.creation_timestamp
1135
1136     def item_extra_kwargs(self, item):
1137         return {'tags': [x.name for x in item.episode.tags.all()], 'soundfile': item, 'episode': item.episode}
1138
1139
1140 podcasts_feed = PodcastsFeed()
1141
1142
1143 class RssNewsFeed(Feed):
1144     title = settings.RADIO_NAME
1145     link = '/news/'
1146     description_template = 'feed/newsitem.html'
1147
1148     def items(self):
1149         return NewsItem.objects.order_by('-date')[:20]
1150
1151     def item_title(self, item):
1152         return item.title
1153
1154     def item_pubdate(self, item):
1155         publication_datetime = datetime.combine(item.date, time(0, 0))
1156         return (
1157             publication_datetime
1158             if publication_datetime > item.creation_timestamp
1159             else item.creation_timestamp
1160         )
1161
1162
1163 rss_news_feed = RssNewsFeed()
1164
1165
1166 class Atom1FeedWithBaseXml(Atom1Feed):
1167     def root_attributes(self):
1168         root_attributes = super().root_attributes()
1169         scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(self.feed['feed_url'])
1170         root_attributes['xml:base'] = urllib.parse.urlunparse((scheme, netloc, '/', params, query, fragment))
1171         return root_attributes
1172
1173
1174 class AtomNewsFeed(RssNewsFeed):
1175     feed_type = Atom1FeedWithBaseXml
1176
1177
1178 atom_news_feed = AtomNewsFeed()
1179
1180
1181 class EmissionPodcastsFeed(PodcastsFeed):
1182     description_template = 'feed/soundfile.html'
1183     feed_type = RssCustomPodcastsFeed
1184
1185     def __call__(self, request, *args, **kwargs):
1186         self.emission = Emission.objects.get(slug=kwargs.get('slug'))
1187         return super().__call__(request, *args, **kwargs)
1188
1189     def item_title(self, item):
1190         if item.fragment:
1191             return '%s - %s' % (item.title, item.episode.title)
1192         return item.episode.title
1193
1194     @property
1195     def title(self):
1196         return self.emission.title
1197
1198     @property
1199     def description(self):
1200         return self.emission.subtitle
1201
1202     @property
1203     def link(self):
1204         return reverse('emission-view', kwargs={'slug': self.emission.slug})
1205
1206     def feed_extra_kwargs(self, obj):
1207         return {'emission': self.emission}
1208
1209     def items(self):
1210         return (
1211             SoundFile.objects.select_related()
1212             .filter(podcastable=True, episode__emission__slug=self.emission.slug)
1213             .order_by('-creation_timestamp')[:50]
1214         )
1215
1216
1217 emission_podcasts_feed = EmissionPodcastsFeed()
1218
1219
1220 class Party(TemplateView):
1221     template_name = 'party.html'
1222
1223     def get_context_data(self, **kwargs):
1224         context = super().get_context_data(**kwargs)
1225         t = random.choice(['newsitem'] * 2 + ['emission'] * 3 + ['soundfile'] * 1 + ['episode'] * 2)
1226         focus = Focus()
1227         if t == 'newsitem':
1228             focus.newsitem = (
1229                 NewsItem.objects.exclude(image__isnull=True).exclude(image__exact='').order_by('?')[0]
1230             )
1231         elif t == 'emission':
1232             focus.emission = (
1233                 Emission.objects.exclude(image__isnull=True).exclude(image__exact='').order_by('?')[0]
1234             )
1235         elif t == 'episode':
1236             focus.episode = (
1237                 Episode.objects.exclude(image__isnull=True).exclude(image__exact='').order_by('?')[0]
1238             )
1239         elif t == 'soundfile':
1240             focus.soundfile = (
1241                 SoundFile.objects.exclude(episode__image__isnull=True)
1242                 .exclude(episode__image__exact='')
1243                 .order_by('?')[0]
1244             )
1245
1246         context['focus'] = focus
1247
1248         return context
1249
1250
1251 party = Party.as_view()
1252
1253
1254 class Chat(DetailView, EmissionMixin):
1255     model = Emission
1256     template_name = 'chat.html'
1257
1258
1259 chat = cache_control(max_age=15)(Chat.as_view())
1260
1261
1262 def media_hosting(request, location, *args, **kwargs):
1263     local_path = default_storage.path(location)
1264     response = HttpResponse(content_type='')
1265     if os.path.exists(local_path):
1266         response['X-Accel-Redirect'] = settings.OFFSITE_MEDIA_SOUNDS[0] + location
1267     else:
1268         response['X-Accel-Redirect'] = settings.OFFSITE_MEDIA_SOUNDS[1] + location
1269     return response
1270
1271
1272 def versions_json(request):
1273     return JsonResponse({'data': {x.project_name: x.version for x in pkg_resources.WorkingSet()}})