9 from django.core.files.storage import default_storage
10 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
11 from django.core.urlresolvers import reverse, reverse_lazy
12 from django.contrib import messages
13 from django.db.models import Q, Sum
14 from django.http import HttpResponse, HttpResponseRedirect, FileResponse, Http404
15 from django.utils.six import StringIO
16 from django.utils.translation import ugettext_lazy as _
17 from django.views.generic.base import RedirectView, TemplateView
18 from django.views.generic.dates import DayArchiveView
19 from django.views.generic.detail import DetailView
20 from django.views.generic.edit import CreateView, FormView, UpdateView
21 from django.views.generic.list import ListView
23 from .forms import UploadTracksForm, TrackMetaForm, TrackSearchForm, CleanupForm, ZoneSettingsForm
24 from .models import (SomaLogLine, Track, Artist, NonstopFile,
25 ScheduledDiffusion, Jingle, Stream, NonstopZoneSettings)
26 from emissions.models import Nonstop, Diffusion
27 from emissions.utils import period_program
30 from .app_settings import app_settings
33 class SomaDayArchiveView(DayArchiveView):
34 queryset = SomaLogLine.objects.all()
35 date_field = "play_timestamp"
36 make_object_list = True
41 class SomaDayArchiveCsvView(SomaDayArchiveView):
42 def render_to_response(self, context, **response_kwargs):
44 writer = csv.writer(out)
45 for line in context['object_list']:
46 if line.filepath.track:
47 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
49 line.filepath.track.title,
50 line.filepath.track.artist.name,
51 line.filepath.track.language,
52 line.filepath.track.instru and 'instru' or '',
53 line.filepath.track.cfwb and 'cfwb' or '',
54 line.filepath.track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if line.filepath.track.added_to_nonstop_timestamp else '',
57 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
59 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
62 class RedirectTodayView(RedirectView):
63 def get_redirect_url(self, *args, **kwargs):
64 today = datetime.datetime.today()
65 return reverse('archive_day', kwargs={
71 class TrackDetailView(DetailView):
74 def get_context_data(self, **kwargs):
75 ctx = super(TrackDetailView, self).get_context_data(**kwargs)
76 ctx['metadata_form'] = TrackMetaForm(instance=self.object)
79 def post(self, request, *args, **kwargs):
80 assert self.request.user.has_perm('nonstop.add_track')
81 instance = self.get_object()
82 old_nonstop_zones = copy.copy(instance.nonstop_zones.all())
83 form = TrackMetaForm(request.POST, instance=instance)
85 new_nonstop_zones = self.get_object().nonstop_zones.all()
86 if set(old_nonstop_zones) != set(new_nonstop_zones):
87 instance.sync_nonstop_zones()
88 return HttpResponseRedirect('.')
91 class ArtistDetailView(DetailView):
95 class ArtistListView(ListView):
99 class ZoneStats(object):
100 def __init__(self, zone, from_date=None, until_date=None, **kwargs):
102 self.qs = Track.objects.filter(nonstop_zones=self.zone, **kwargs)
103 self.from_date = from_date
105 self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__gte=from_date)
107 self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__lte=until_date)
108 self.qs = self.qs.distinct()
110 def total_duration(self, **kwargs):
112 total = self.qs.filter(**kwargs).aggregate(Sum('duration'))['duration__sum'].total_seconds()
113 except AttributeError:
114 # 'NoneType' object has no attribute 'total_seconds', if there's no
118 duration = _('%d hours') % (total / 3600)
120 duration = _('%d minutes') % (total / 60)
121 start = datetime.datetime(2000, 1, 1, self.zone.start.hour, self.zone.start.minute)
122 end = datetime.datetime(2000, 1, 1, self.zone.end.hour, self.zone.end.minute)
124 end = end + datetime.timedelta(days=1)
125 return duration + _(', → %d days') % (total // (end - start).total_seconds())
127 def count(self, **kwargs):
128 return self.qs.filter(**kwargs).count()
130 def percentage(self, **kwargs):
134 return '%.2f%%' % (100. * self.count(**kwargs) / total)
137 return self.count(instru=True)
139 def instru_percentage(self):
140 return self.percentage(instru=True)
143 return self.count(sabam=True)
145 def sabam_percentage(self):
146 return self.percentage(sabam=True)
149 return self.count(cfwb=True)
151 def cfwb_percentage(self):
152 return self.percentage(cfwb=True)
154 def language_set(self):
155 return self.count() - self.language_unset()
157 def language_unset(self):
158 return self.count(language='')
160 def unset_language_percentage(self):
161 return self.percentage(language='')
164 return self.count(language='fr')
166 def unset_or_na_language(self):
167 return self.qs.filter(Q(language='') | Q(language='na')).count()
169 def french_percentage(self):
170 considered_tracks = self.count() - self.unset_or_na_language()
171 if considered_tracks == 0:
173 return '%.2f%%' % (100. * self.french() / considered_tracks)
175 def quota_french(self):
176 # obligation de diffuser annuellement au moins 30% d'œuvres musicales de
178 considered_tracks = self.count() - self.unset_or_na_language()
179 if considered_tracks == 0:
181 return (100. * self.french() / considered_tracks) > 30.
183 def quota_cfwb(self):
184 # obligation de diffuser annuellement au moins 4,5% d'œuvres musicales
185 # émanant de la Communauté française
186 considered_tracks = self.count()
187 if considered_tracks == 0:
189 return (100. * self.cfwb() / considered_tracks) > 4.5
192 return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
194 def percent_new_files(self):
195 return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
198 def parse_date(date):
199 if date.endswith('d'):
200 return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
201 return datetime.datetime.strptime(date, '%Y-%m-%d').date()
204 class StatisticsView(TemplateView):
205 template_name = 'nonstop/statistics.html'
207 def get_context_data(self, **kwargs):
208 context = super(StatisticsView, self).get_context_data(**kwargs)
209 context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
211 if 'from' in self.request.GET:
212 kwargs['from_date'] = parse_date(self.request.GET['from'])
213 context['from_date'] = kwargs['from_date']
214 if 'until' in self.request.GET:
215 kwargs['until_date'] = parse_date(self.request.GET['until'])
216 if 'onair' in self.request.GET:
217 kwargs['nonstopfile__somalogline__on_air'] = True
218 for zone in context['zones']:
219 zone.stats = ZoneStats(zone, **kwargs)
223 class UploadTracksView(FormView):
224 form_class = UploadTracksForm
225 template_name = 'nonstop/upload.html'
228 def post(self, request, *args, **kwargs):
229 assert self.request.user.has_perm('nonstop.add_track')
230 form_class = self.get_form_class()
231 form = self.get_form(form_class)
232 tracks = request.FILES.getlist('tracks')
233 if not form.is_valid():
234 return self.form_invalid(form)
235 missing_metadata = []
238 with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
239 tmpfile.write(f.read())
241 metadata = mutagen.File(tmpfile.name, easy=True)
242 if not metadata or not metadata.get('artist') or not metadata.get('title'):
243 missing_metadata.append(f.name)
245 metadatas[f.name] = metadata
247 form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
248 return self.form_invalid(form)
251 metadata = metadatas[f.name]
252 artist_name = metadata.get('artist')[0]
253 track_title = metadata.get('title')[0]
255 monthdir = datetime.datetime.today().strftime('%Y-%m')
256 filepath = '%s/%s - %s - %s%s' % (monthdir,
257 datetime.datetime.today().strftime('%y%m%d'),
258 artist_name[:50].replace('/', ' ').strip(),
259 track_title[:80].replace('/', ' ').strip(),
260 os.path.splitext(f.name)[-1])
262 artist, created = Artist.objects.get_or_create(name=artist_name)
263 track, created = Track.objects.get_or_create(title=track_title, artist=artist,
264 defaults={'uploader': self.request.user})
265 if created or not track.file_exists():
266 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
267 nonstop_file = NonstopFile()
268 nonstop_file.set_track_filepath(filepath)
269 nonstop_file.track = track
272 # don't keep duplicated file and do not create a duplicated nonstop file object
274 if request.POST.get('nonstop_zone'):
275 track.nonstop_zones.add(
276 Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
277 track.sync_nonstop_zones()
279 messages.info(self.request, '%d new track(s)' % len(tracks))
280 return self.form_valid(form)
283 class TracksMetadataView(ListView):
284 template_name = 'nonstop/tracks_metadata.html'
286 def get_context_data(self, **kwargs):
287 context = super().get_context_data(**kwargs)
288 context['view'] = self
291 def get_queryset(self):
292 return Track.objects.exclude(nonstop_zones__isnull=True)
294 def post(self, request, *args, **kwargs):
295 assert self.request.user.has_perm('nonstop.add_track')
296 for track_id in request.POST.getlist('track'):
297 track = Track.objects.get(id=track_id)
298 track.language = request.POST.get('lang-%s' % track_id, '')
299 track.instru = 'instru-%s' % track_id in request.POST
300 track.sabam = 'sabam-%s' % track_id in request.POST
301 track.cfwb = 'cfwb-%s' % track_id in request.POST
303 return HttpResponseRedirect('.')
306 class RandomTracksMetadataView(TracksMetadataView):
307 page_title = _('Metadata of random tracks')
309 def get_queryset(self):
310 return super().get_queryset().filter(Q(language='') | Q(language__isnull=True)).order_by('?')[:50]
313 class RecentTracksMetadataView(TracksMetadataView):
314 page_title = _('Metadata of recent tracks')
316 def get_queryset(self):
317 return super().get_queryset().exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
320 class ArtistTracksMetadataView(TracksMetadataView):
323 def page_title(self):
324 return _('Metadata of tracks from %s') % Artist.objects.get(id=self.kwargs['artist_pk']).name
326 def get_queryset(self):
327 return Track.objects.filter(artist_id=self.kwargs['artist_pk']).order_by('title')
330 class QuickLinksView(TemplateView):
331 template_name = 'nonstop/quick_links.html'
333 def get_context_data(self, **kwargs):
334 context = super().get_context_data(**kwargs)
335 day = datetime.datetime.today()
336 context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
340 class SearchView(TemplateView):
341 template_name = 'nonstop/search.html'
343 def get_queryset(self):
344 queryset = Track.objects.all()
346 q = self.request.GET.get('q')
348 queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
350 zone = self.request.GET.get('zone')
352 from emissions.models import Nonstop
354 queryset = queryset.filter(nonstop_zones=None)
356 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
358 queryset = queryset.filter(nonstop_zones=zone)
360 order = self.request.GET.get('order_by') or 'title'
362 if 'added_to_nonstop_timestamp' in order:
363 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
364 queryset = queryset.order_by(order)
367 def get_context_data(self, **kwargs):
368 ctx = super(SearchView, self).get_context_data(**kwargs)
369 ctx['form'] = TrackSearchForm(self.request.GET)
370 queryset = self.get_queryset()
371 qs = self.request.GET.copy()
373 ctx['qs'] = qs.urlencode()
375 tracks = Paginator(queryset.select_related(), 20)
377 page = self.request.GET.get('page')
379 ctx['tracks'] = tracks.page(page)
380 except PageNotAnInteger:
381 ctx['tracks'] = tracks.page(1)
383 ctx['tracks'] = tracks.page(tracks.num_pages)
388 class SearchCsvView(SearchView):
389 def get(self, request, *args, **kwargs):
391 writer = csv.writer(out)
392 writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
393 for track in self.get_queryset():
395 track.title if track.title else 'Inconnu',
396 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
397 ' + '.join([x.title for x in track.nonstop_zones.all()]),
398 track.language or '',
399 track.instru and 'instru' or '',
400 track.cfwb and 'cfwb' or '',
401 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
403 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
406 class CleanupView(TemplateView):
407 template_name = 'nonstop/cleanup.html'
409 def get_context_data(self, **kwargs):
410 ctx = super(CleanupView, self).get_context_data(**kwargs)
411 ctx['form'] = CleanupForm()
413 zone = self.request.GET.get('zone')
415 from emissions.models import Nonstop
416 ctx['zone'] = Nonstop.objects.get(id=zone)
417 ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
418 ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
419 'added_to_nonstop_timestamp').select_related()[:30]
422 def post(self, request, *args, **kwargs):
423 assert self.request.user.has_perm('nonstop.add_track')
425 for track_id in request.POST.getlist('track'):
426 if request.POST.get('remove-%s' % track_id):
427 track = Track.objects.get(id=track_id)
428 track.nonstop_zones.clear()
429 track.sync_nonstop_zones()
432 messages.info(self.request, 'Removed %d new track(s)' % count)
433 return HttpResponseRedirect('.')
436 class AddSomaDiffusionView(CreateView):
437 model = ScheduledDiffusion
438 fields = ['jingle', 'stream']
439 template_name = 'nonstop/streamed-diffusion.html'
443 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
445 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
446 fields.append('stream')
449 def get_initial(self):
450 initial = super(AddSomaDiffusionView, self).get_initial()
451 initial['jingle'] = None
452 if 'stream' in self.fields:
453 initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
454 initial['stream'] = Stream.objects.all().first()
457 def form_valid(self, form):
458 form.instance.diffusion_id = self.kwargs['pk']
459 episode = form.instance.diffusion.episode
460 if 'stream' in self.fields and form.instance.stream_id is None:
461 messages.error(self.request, _('missing stream'))
462 return HttpResponseRedirect(reverse('episode-view', kwargs={
463 'emission_slug': episode.emission.slug,
464 'slug': episode.slug}))
465 response = super(AddSomaDiffusionView, self).form_valid(form)
466 messages.info(self.request, _('%s added to schedule') % episode.emission.title)
469 def get_success_url(self):
470 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
471 episode = diffusion.episode
472 return reverse('episode-view', kwargs={
473 'emission_slug': episode.emission.slug,
474 'slug': episode.slug})
477 class DelSomaDiffusionView(RedirectView):
478 def get_redirect_url(self, pk):
479 soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
480 episode = soma_diffusion.episode
481 ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
482 messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
483 return reverse('episode-view', kwargs={
484 'emission_slug': episode.emission.slug,
485 'slug': episode.slug})
488 class DiffusionPropertiesView(UpdateView):
489 model = ScheduledDiffusion
490 fields = ['jingle', 'stream']
491 template_name = 'nonstop/streamed-diffusion.html'
495 diffusion = self.get_object().diffusion
497 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
498 fields.append('stream')
501 def form_valid(self, form):
502 episode = self.get_object().diffusion.episode
503 if 'stream' in self.fields and form.instance.stream_id is None:
504 messages.error(self.request, _('missing stream'))
505 return HttpResponseRedirect(reverse('episode-view', kwargs={
506 'emission_slug': episode.emission.slug,
507 'slug': episode.slug}))
508 response = super(DiffusionPropertiesView, self).form_valid(form)
509 messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
512 def get_success_url(self):
513 episode = self.get_object().diffusion.episode
514 return reverse('episode-view', kwargs={
515 'emission_slug': episode.emission.slug,
516 'slug': episode.slug})
519 def jingle_audio_view(request, *args, **kwargs):
520 jingle = Jingle.objects.get(id=kwargs['pk'])
521 return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
524 class AjaxProgram(TemplateView):
525 template_name = 'nonstop/program-fragment.html'
527 def get_context_data(self, date, **kwargs):
528 context = super().get_context_data(**kwargs)
529 now = datetime.datetime.now()
531 date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
533 date_start = datetime.datetime.today()
534 date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
535 today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
536 date_end = date_start + datetime.timedelta(days=1)
537 context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
538 for x in context['day_program']:
539 x.klass = x.__class__.__name__
541 for i, x in enumerate(context['day_program']):
542 if today and x.datetime > now and previous_prog:
543 previous_prog.now = True
549 class ZoneSettings(FormView):
550 form_class = ZoneSettingsForm
551 template_name = 'nonstop/zone_settings.html'
552 success_url = reverse_lazy('nonstop-quick-links')
554 def get_context_data(self, **kwargs):
555 context = super().get_context_data(**kwargs)
556 context['zone'] = Nonstop.objects.get(slug=self.kwargs['slug'])
559 def get_initial(self):
561 zone = Nonstop.objects.get(slug=self.kwargs['slug'])
562 except Nonstop.DoesNotExist:
564 zone_settings = zone.nonstopzonesettings_set.first()
565 if zone_settings is None:
566 zone_settings = NonstopZoneSettings(nonstop=zone)
568 initial = super().get_initial()
569 initial['start'] = zone.start.strftime('%H:%M') if zone.start else None
570 initial['end'] = zone.end.strftime('%H:%M') if zone.end else None
571 initial['intro_jingle'] = zone_settings.intro_jingle_id
572 initial['jingles'] = [x.id for x in zone_settings.jingles.all()]
575 def form_valid(self, form):
576 zone = Nonstop.objects.get(slug=self.kwargs['slug'])
577 zone_settings = zone.nonstopzonesettings_set.first()
578 zone.start = form.cleaned_data['start']
579 zone.end = form.cleaned_data['end']
580 zone_settings.jingles.set(form.cleaned_data['jingles'])
581 zone_settings.intro_jingle_id = form.cleaned_data['intro_jingle']
584 return super().form_valid(form)
587 class MuninTracks(StatisticsView):
588 template_name = 'nonstop/munin_tracks.txt'
589 content_type = 'text/plain; charset=utf-8'
591 def get_context_data(self, **kwargs):
592 context = super().get_context_data(**kwargs)
593 context['nonstop_general_total'] = Track.objects.count()
594 active_tracks_qs = Track.objects.filter(nonstop_zones__isnull=False).distinct()
595 context['nonstop_general_active'] = active_tracks_qs.count()
596 context['nonstop_percentages_instru'] = 100 * (
597 active_tracks_qs.filter(instru=True).count() /
598 context['nonstop_general_active'])
599 context['nonstop_percentages_cfwb'] = 100 * (
600 active_tracks_qs.filter(cfwb=True).count() /
601 context['nonstop_general_active'])
602 context['nonstop_percentages_langset'] = 100 * (
603 active_tracks_qs.exclude(language='').count() /
604 context['nonstop_general_active'])
605 context['nonstop_percentages_french'] = 100 * (
606 active_tracks_qs.filter(language='fr').count() /
607 active_tracks_qs.exclude(language__isnull=True).exclude(language__in=('', 'na')).count())