6 from datetime import date, datetime, time, timedelta
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 (
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
53 def get_emission_context(self, emission, episode_ids=None):
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)
62 episodes_queryset = episodes_queryset.filter(emission=emission)
64 if settings.USE_AGENDA_ONLY_FIELD:
65 episodes_queryset = episodes_queryset.exclude(agenda_only=True)
67 context['episodes'] = (
68 episodes_queryset.extra(
70 'first_diffusion': 'emissions_diffusion.datetime',
72 select_params=(False, True),
74 '''datetime = (SELECT MIN(datetime)
75 FROM emissions_diffusion
76 WHERE episode_id = emissions_episode.id
77 AND datetime <= CURRENT_TIMESTAMP)'''
79 tables=['emissions_diffusion'],
81 .order_by('-first_diffusion')
85 context['all_episodes'] = (
86 episodes_queryset.extra(
88 'first_diffusion': 'emissions_diffusion.datetime',
90 select_params=(False, True),
92 '''datetime = (SELECT MIN(datetime)
93 FROM emissions_diffusion
94 WHERE episode_id = emissions_episode.id)'''
96 tables=['emissions_diffusion'],
98 .order_by('-first_diffusion')
102 context['futurEpisodes'] = (
103 episodes_queryset.extra(
105 'first_diffusion': 'emissions_diffusion.datetime',
107 select_params=(False, True),
109 '''datetime = (SELECT MIN(datetime)
110 FROM emissions_diffusion
111 WHERE episode_id = emissions_episode.id
112 AND datetime > CURRENT_TIMESTAMP)'''
114 tables=['emissions_diffusion'],
116 .order_by('first_diffusion')
120 # get all related soundfiles in a single query
122 if episode_ids is not None:
123 for episode_id in episode_ids:
124 soundfiles[episode_id] = None
126 for episode in Episode.objects.filter(emission=emission):
127 soundfiles[episode.id] = None
129 for soundfile in SoundFile.objects.select_related().filter(
130 podcastable=True, fragment=False, episode__emission=emission
132 soundfiles[soundfile.episode_id] = soundfile
134 Episode.set_prefetched_soundfiles(soundfiles)
136 # context['futurEpisodes'] = context['episodes'].filter(first_diffusion='2013')[0:3]
141 class EmissionDetailView(DetailView, EmissionMixin):
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')
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]
157 nonstop_object = Nonstop.objects.get(slug=self.object.slug)
158 except Nonstop.DoesNotExist:
162 dates = [today - timedelta(days=x) for x in range(7)]
163 if datetime.now().time() < nonstop_object.start:
165 context['nonstop'] = nonstop_object
166 context['nonstop_dates'] = dates
167 context.update(self.get_emission_context(self.object))
171 emission = EmissionDetailView.as_view()
174 class EpisodeDetailView(EmissionEpisodeMixin, DetailView, EmissionMixin):
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')
183 context['emission'] = context['episode'].emission
184 except Emission.DoesNotExist:
186 if self.kwargs.get('emission_slug') != context['emission'].slug:
188 context.update(self.get_emission_context(context['emission']))
189 context['topik_pages'] = [x.page for x in ItemTopik.objects.filter(episode=self.object)]
193 episode = EpisodeDetailView.as_view()
196 class NonstopPlaylistView(TemplateView):
197 template_name = 'nonstop_playlist.html'
199 def get_context_data(self, **kwargs):
200 context = super().get_context_data(**kwargs)
202 context['date'] = date(int(kwargs.get('year')), int(kwargs.get('month')), int(kwargs.get('day')))
205 context['future'] = context['date'] >= date.today()
207 context['emission'] = Emission.objects.filter(slug=kwargs.get('slug')).first()
209 nonstop_object = Nonstop.objects.get(slug=kwargs.get('slug'))
210 except Nonstop.DoesNotExist:
212 context['nonstop'] = nonstop_object
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,
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,
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)
237 nonstop_playlist = NonstopPlaylistView.as_view()
240 class EmissionEpisodesDetailView(DetailView, EmissionMixin):
242 template_name = 'emissions/episodes.html'
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')
250 for schedule in context['schedules']:
253 count_per_month += bin(schedule.weeks).count('1')
255 context['count_per_month'] = count_per_month
257 context['search_query'] = self.request.GET.get('q')
258 if context['search_query']:
259 if settings.USE_HAYSTACK:
264 .filter(emission_slug_exact=self.object.slug, text=context['search_query'])
266 episode_ids = [x.pk for x in sqs]
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)
278 context.update(self.get_emission_context(self.object, episode_ids=episode_ids))
282 emissionEpisodes = EmissionEpisodesDetailView.as_view()
285 class SoundFileEmbedView(DetailView):
287 template_name = 'soundfiles/embed.html'
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:
293 if self.kwargs.get('emission_slug') != self.object.episode.emission.slug:
295 context['episode'] = self.object.episode
299 soundfile_embed = SoundFileEmbedView.as_view()
302 class EpisodeEmbedRedirect(RedirectView):
303 def get_redirect_url(self, **kwargs):
305 soundfile = SoundFile.objects.get(
306 episode__slug=kwargs['episode_slug'],
307 episode__emission__slug=kwargs['emission_slug'],
311 except SoundFile.DoesNotExist:
313 kwargs['pk'] = soundfile.id
314 return reverse('soundfile-embed-view', kwargs=kwargs)
317 episode_embed_redirect = EpisodeEmbedRedirect.as_view()
320 class SoundFileDialogEmbedView(DetailView):
322 template_name = 'soundfiles/dialog-embed.html'
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:
328 if self.kwargs.get('emission_slug') != self.object.episode.emission.slug:
330 context['episode'] = self.object.episode
334 soundfile_dlg_embed = SoundFileDialogEmbedView.as_view()
337 class ProgramView(TemplateView):
338 template_name = 'program.html'
340 def get_context_data(self, year=None, week=None, **kwargs):
341 context = super().get_context_data(**kwargs)
343 context['weekday'] = datetime.today().weekday()
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:
349 context['week_first_day'] = utils.tofirstdayinisoweek(year, week)
350 context['week_last_day'] = context['week_first_day'] + timedelta(days=6)
355 program = ProgramView.as_view()
364 def __init__(self, i, j):
369 def add_schedule(self, schedule):
370 same_emission_and_duration = [
372 for x in self.schedules
373 if (x.emission_id == schedule.emission_id and x.get_duration() == schedule.get_duration())
375 if same_emission_and_duration:
376 # add extra week/s to existing schedule
377 same_emission_and_duration[0].weeks |= schedule.weeks
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,
386 self.schedules.append(schedule)
388 def sorted_schedules(self):
389 return sorted(self.schedules, key=lambda x: x.week_sort_key())
393 return ', '.join([x.emission.title for x in self.schedules])
397 def __eq__(self, other):
398 return force_str(self) == force_str(other) and self.time_label == other.time_label
401 class Grid(TemplateView):
402 template_name = 'grid.html'
404 def get_context_data(self, **kwargs):
405 context = super().get_context_data(**kwargs)
407 nb_lines = 2 * 24 # the cells are half hours
410 times = ['%02d:%02d' % (x / 2, x % 2 * 30) for x in range(nb_lines)]
411 # start grid after the night programs
414 2 * emissions_app_settings.DAY_HOUR_START
415 + (1 if emissions_app_settings.DAY_MINUTE_START else 0) :
418 : 2 * emissions_app_settings.DAY_HOUR_START
419 + (1 if emissions_app_settings.DAY_MINUTE_START else 0)
424 for nonstop in Nonstop.objects.all():
425 if nonstop.start == nonstop.end:
427 if nonstop.start < nonstop.end:
430 nonstop.start.hour + nonstop.start.minute / 60.0,
431 nonstop.end.hour + nonstop.end.minute / 60.0,
432 nonstop.get_public_label(),
441 nonstop.start.hour + nonstop.start.minute / 60.0,
443 nonstop.get_public_label(),
451 nonstop.end.hour + nonstop.end.minute / 60.0,
452 nonstop.get_public_label(),
459 for i in range(nb_lines):
462 grid[-1].append(TimeCell(i, j))
465 nonstop = [x for x in nonstops if i >= x[0] * 2 and i < x[1] * 2][0]
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
474 == emissions_app_settings.DAY_HOUR_START + emissions_app_settings.DAY_MINUTE_START / 60
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' % (
481 emissions_app_settings.DAY_MINUTE_START,
485 Schedule.objects.prefetch_related('emission__categories').select_related().order_by('datetime')
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()
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)
498 # start grid after the night programs
501 2 * emissions_app_settings.DAY_HOUR_START
502 + (1 if emissions_app_settings.DAY_MINUTE_START else 0) :
505 : 2 * emissions_app_settings.DAY_HOUR_START
506 + (1 if emissions_app_settings.DAY_MINUTE_START else 0)
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:
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:
527 # here it is, same cell, same emission, several
529 schedule_list.sort(key=lambda x: x.get_duration())
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,
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,)
548 # different durations, also append other
550 schedule_list[0].time_label_extra = ', -%02d:%02d %s' % (
553 schedule.weeks_string,
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:
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:
571 grid[i][j + t] = None
576 # once we're done we remove empty cells
577 grid[i] = [x for x in grid[i] if x is not None]
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:
589 # we look if the next time cell has the same emissions
592 for bj, x in enumerate(grid[i + cell.h])
593 if x == cell and x.y == cell.y and x.w == cell.w
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]
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.
607 # - 7am30, seuls contre tout, 1h30
608 # - 8am, du pied gauche & la voix de la rue, 1h
609 # should produce: (this is case A)
611 # | seuls contre tout |
612 # |---------------------|
615 # | la voix de la rue |
617 # On the other hand, if all three emissions started
618 # at 7am30, we want: (this is case B)
620 # | seuls contre tout |
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}
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:
633 same_cell_below = same_cell_below[0]
634 same_cell_below_emissions = {x.emission for x in same_cell_below.schedules}
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
644 # this handles case B (see comment above)
645 # we set the cell time label to the longest
647 grid[i][j].time_label = same_cell_below.time_label
648 # then we sort emissions so the longest are
650 grid[i][j].schedules.sort(key=lambda x: -x.get_duration())
651 # then we add individual time labels to the
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()
658 schedule.time_label = '%02d:%02d-%02d:%02d' % (
659 schedule.datetime.hour,
660 schedule.datetime.minute,
665 grid[i + cursor].remove(same_cell_below)
666 elif same_cell_below_emissions and current_cell_emissions.intersection(
667 same_cell_below_emissions
669 same_cell_below.schedules = [
671 for x in same_cell_below.schedules
672 if x.emission not in current_cell_emissions or x.get_duration() < 30
679 # cut late night hours
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)]
692 grid = Grid.as_view()
695 class Home(TemplateView):
696 template_name = 'home.html'
698 def dispatch(self, request, *args, **kwargs):
699 page = Page.objects.filter(slug='index', parent__isnull=True).first()
701 return publish_page(request, page)
702 return super().dispatch(request, *args, **kwargs)
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
709 context['newsitems'] = NewsItem.objects.exclude(date__gt=date.today()).order_by('-date')[
710 : settings.HOME_NEWSITEMS_COUNT
713 context['soundfiles'] = (
714 SoundFile.objects.prefetch_related('episode__emission__categories')
715 .filter(podcastable=True, fragment=False)
717 creation_timestamp__lt=datetime.now() - timedelta(minutes=settings.PODCASTS_PUBLICATION_DELAY)
722 'first_diffusion': 'emissions_diffusion.datetime',
724 select_params=(False, True),
726 '''datetime = (SELECT MIN(datetime)
727 FROM emissions_diffusion
728 WHERE episode_id = emissions_episode.id)'''
730 tables=['emissions_diffusion'],
732 .order_by('-creation_timestamp')
733 .distinct()[: settings.HOME_PODCASTS_COUNT]
736 context['newsletter_form'] = SubscribeForm()
741 home = Home.as_view()
744 class NewsItemView(DetailView):
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)]
755 newsitemview = NewsItemView.as_view()
758 class News(TemplateView):
759 template_name = 'news.html'
761 def get_context_data(self, **kwargs):
762 context = super().get_context_data(**kwargs)
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]
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())
779 news = News.as_view()
782 class Agenda(TemplateView):
783 template_name = 'agenda.html'
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]
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)
797 agenda = Agenda.as_view()
800 class AgendaByMonth(MonthArchiveView):
801 template_name = 'agenda.html'
802 queryset = NewsItem.objects.filter(event_date__isnull=False)
804 date_field = 'event_date'
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')
814 agenda_by_month = AgendaByMonth.as_view()
817 class Emissions(TemplateView):
818 template_name = 'emissions.html'
820 def get_queryset(self):
821 return Emission.objects.prefetch_related('categories').filter(archived=False).order_by('title')
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()
830 emissions = Emissions.as_view()
833 class EmissionsArchives(TemplateView):
834 template_name = 'emissions/archives.html'
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')
841 context['categories'] = Category.objects.all()
845 emissionsArchives = EmissionsArchives.as_view()
848 class Listen(TemplateView):
849 template_name = 'listen.html'
851 def get_context_data(self, **kwargs):
852 context = super().get_context_data(**kwargs)
854 SoundFile.objects.prefetch_related('episode__emission__categories')
855 .filter(podcastable=True, got_focus__isnull=False)
859 'first_diffusion': 'emissions_diffusion.datetime',
861 select_params=(False, True),
863 '''datetime = (SELECT MIN(datetime)
864 FROM emissions_diffusion
865 WHERE episode_id = emissions_episode.id)'''
867 tables=['emissions_diffusion'],
869 .order_by('-first_diffusion')
872 context['soundfiles'] = (
873 SoundFile.objects.prefetch_related('episode__emission__categories')
874 .filter(podcastable=True)
878 'first_diffusion': 'emissions_diffusion.datetime',
880 select_params=(False, True),
882 '''datetime = (SELECT MIN(datetime)
883 FROM emissions_diffusion
884 WHERE episode_id = emissions_episode.id)'''
886 tables=['emissions_diffusion'],
888 .order_by('-creation_timestamp')
895 listen = Listen.as_view()
899 def get_infos(self, ctx):
901 include_track_metadata = settings.ONAIR_ALWAYS_INCLUDE_TRACK_METADATA
903 if ctx.get('episode'):
905 'title': ctx['episode'].title,
906 'subtitle': ctx['episode'].subtitle,
907 'url': ctx['episode'].get_absolute_url(),
910 if ctx.get('emission'):
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(),
922 hasattr(ctx['current_slot'], 'recurringplaylistdiffusion_set')
923 and ctx['current_slot'].recurringplaylistdiffusion_set.exists()
925 include_track_metadata = True
927 if ctx.get('nonstop'):
928 include_track_metadata = True
929 redirect_path = ctx['nonstop'].redirect_path
931 'title': ctx['nonstop'].get_public_label(),
932 'slug': ctx['current_slot'].slug,
935 infos['nonstop']['url'] = redirect_path
936 today = datetime.today()
937 infos['nonstop']['playlist_url'] = reverse(
941 'month': today.month,
943 'slug': ctx['current_slot'].slug,
947 if include_track_metadata:
948 infos.update(get_current_nonstop_track())
952 def get(self, request, *args, **kwargs):
953 infos = self.get_infos(ctx=whatsonair())
954 return JsonResponse({'data': infos})
957 onair = cache_control(max_age=15)(csrf_exempt(OnAir.as_view()))
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
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
971 hasattr(ctx['current_slot'], 'recurringplaylistdiffusion_set')
972 and ctx['current_slot'].recurringplaylistdiffusion_set.exists()
974 include_track_metadata = True
975 elif ctx.get('nonstop'):
976 include_track_metadata = True
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
990 def get(self, request, *args, **kwargs):
991 infos = self.get_infos(ctx=whatsonair())
992 return JsonResponse({'data': infos})
995 dab_service = cache_control(max_age=15)(csrf_exempt(DabService.as_view()))
998 class NewsItemDetailView(DetailView):
1002 newsitem = NewsItemDetailView.as_view()
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(
1013 settings.PODCAST_IMAGE_GEOMETRY,
1014 **settings.PODCAST_IMAGE_THUMBNAIL_OPTIONS,
1017 image_url = emission.image.url
1019 image_url = settings.PODCASTS_DEFAULT_IMAGE_PATH
1020 image_url = urllib.parse.urljoin(self.feed['link'], image_url)
1021 handler.startElement('image', {})
1023 handler.addQuickElement('title', emission.title)
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})
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})
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)
1042 handler.addQuickElement('itunes:email', settings.DEFAULT_FROM_EMAIL)
1043 handler.addQuickElement('itunes:name', settings.RADIO_NAME)
1044 handler.endElement('itunes:owner')
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'
1052 def add_item_elements(self, handler, item):
1053 super().add_item_elements(handler, item)
1055 for tag in item.get('tags') or []:
1056 handler.addQuickElement('dc:subject', tag)
1057 if tag == 'explicit':
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
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(
1077 % (soundfile.duration / 3600, soundfile.duration % 3600 / 60, soundfile.duration % 60),
1081 class PodcastsFeed(Feed):
1082 title = '%s - Podcasts' % settings.RADIO_NAME
1084 description_template = 'feed/soundfile.html'
1085 feed_type = RssCustomPodcastsFeed
1087 def get_feed(self, obj, request):
1088 self.request = request
1089 return super().get_feed(obj, request)
1092 def description(self):
1093 return settings.RADIO_META_DESCRIPTION
1097 SoundFile.objects.select_related()
1098 .filter(podcastable=True)
1100 creation_timestamp__lt=datetime.now() - timedelta(minutes=settings.PODCASTS_PUBLICATION_DELAY)
1102 .exclude(file__isnull=True)
1104 .order_by('-creation_timestamp')[:50]
1107 def item_title(self, item):
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)
1112 def item_link(self, item):
1114 return item.episode.get_absolute_url() + '#%s' % item.id
1115 return item.episode.get_absolute_url()
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())
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')
1126 return os.stat(sound_path)[stat.ST_SIZE]
1130 def item_enclosure_mime_type(self, item):
1133 def item_pubdate(self, item):
1134 return item.creation_timestamp
1136 def item_extra_kwargs(self, item):
1137 return {'tags': [x.name for x in item.episode.tags.all()], 'soundfile': item, 'episode': item.episode}
1140 podcasts_feed = PodcastsFeed()
1143 class RssNewsFeed(Feed):
1144 title = settings.RADIO_NAME
1146 description_template = 'feed/newsitem.html'
1149 return NewsItem.objects.order_by('-date')[:20]
1151 def item_title(self, item):
1154 def item_pubdate(self, item):
1155 publication_datetime = datetime.combine(item.date, time(0, 0))
1157 publication_datetime
1158 if publication_datetime > item.creation_timestamp
1159 else item.creation_timestamp
1163 rss_news_feed = RssNewsFeed()
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
1174 class AtomNewsFeed(RssNewsFeed):
1175 feed_type = Atom1FeedWithBaseXml
1178 atom_news_feed = AtomNewsFeed()
1181 class EmissionPodcastsFeed(PodcastsFeed):
1182 description_template = 'feed/soundfile.html'
1183 feed_type = RssCustomPodcastsFeed
1185 def __call__(self, request, *args, **kwargs):
1186 self.emission = Emission.objects.get(slug=kwargs.get('slug'))
1187 return super().__call__(request, *args, **kwargs)
1189 def item_title(self, item):
1191 return '%s - %s' % (item.title, item.episode.title)
1192 return item.episode.title
1196 return self.emission.title
1199 def description(self):
1200 return self.emission.subtitle
1204 return reverse('emission-view', kwargs={'slug': self.emission.slug})
1206 def feed_extra_kwargs(self, obj):
1207 return {'emission': self.emission}
1211 SoundFile.objects.select_related()
1212 .filter(podcastable=True, episode__emission__slug=self.emission.slug)
1213 .order_by('-creation_timestamp')[:50]
1217 emission_podcasts_feed = EmissionPodcastsFeed()
1220 class Party(TemplateView):
1221 template_name = 'party.html'
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)
1229 NewsItem.objects.exclude(image__isnull=True).exclude(image__exact='').order_by('?')[0]
1231 elif t == 'emission':
1233 Emission.objects.exclude(image__isnull=True).exclude(image__exact='').order_by('?')[0]
1235 elif t == 'episode':
1237 Episode.objects.exclude(image__isnull=True).exclude(image__exact='').order_by('?')[0]
1239 elif t == 'soundfile':
1241 SoundFile.objects.exclude(episode__image__isnull=True)
1242 .exclude(episode__image__exact='')
1246 context['focus'] = focus
1251 party = Party.as_view()
1254 class Chat(DetailView, EmissionMixin):
1256 template_name = 'chat.html'
1259 chat = cache_control(max_age=15)(Chat.as_view())
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
1268 response['X-Accel-Redirect'] = settings.OFFSITE_MEDIA_SOUNDS[1] + location
1272 def versions_json(request):
1273 return JsonResponse({'data': {x.project_name: x.version for x in pkg_resources.WorkingSet()}})