]> git.0d.be Git - django-panik-nonstop.git/blob - nonstop/views.py
481ed133e83ca74ad2d36973e0a2a9ca7fd69d92
[django-panik-nonstop.git] / nonstop / views.py
1 import copy
2 import csv
3 import datetime
4 import os
5 import tempfile
6
7 import mutagen
8
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
22
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
28
29 from . import utils
30 from .app_settings import app_settings
31
32
33 class SomaDayArchiveView(DayArchiveView):
34     queryset = SomaLogLine.objects.all()
35     date_field = "play_timestamp"
36     make_object_list = True
37     allow_future = False
38     month_format = '%m'
39
40
41 class SomaDayArchiveCsvView(SomaDayArchiveView):
42     def render_to_response(self, context, **response_kwargs):
43         out = StringIO()
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'),
48                     line.filepath.short,
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 '',
55                     ])
56             else:
57                 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
58                                 line.filepath.short])
59         return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
60
61
62 class RedirectTodayView(RedirectView):
63     def get_redirect_url(self, *args, **kwargs):
64         today = datetime.datetime.today()
65         return reverse('archive_day', kwargs={
66                         'year': today.year,
67                         'month': today.month,
68                         'day': today.day})
69
70
71 class TrackDetailView(DetailView):
72     model = Track
73
74     def get_context_data(self, **kwargs):
75         ctx = super(TrackDetailView, self).get_context_data(**kwargs)
76         ctx['metadata_form'] = TrackMetaForm(instance=self.object)
77         return ctx
78
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)
84         form.save()
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('.')
89
90
91 class ArtistDetailView(DetailView):
92     model = Artist
93
94
95 class ArtistListView(ListView):
96     model = Artist
97
98
99 class ZoneStats(object):
100     def __init__(self, zone, from_date=None, until_date=None, **kwargs):
101         self.zone = zone
102         self.qs = Track.objects.filter(nonstop_zones=self.zone, **kwargs)
103         self.from_date = from_date
104         if from_date:
105             self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__gte=from_date)
106         if until_date:
107             self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__lte=until_date)
108         self.qs = self.qs.distinct()
109
110     def total_duration(self, **kwargs):
111         try:
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
115             # track in queryset
116             return '-'
117         if total > 3600 * 2:
118             duration = _('%d hours') % (total / 3600)
119         else:
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)
123         if end < start:
124             end = end + datetime.timedelta(days=1)
125         return duration + _(', → %d days') % (total // (end - start).total_seconds())
126
127     def count(self, **kwargs):
128         return self.qs.filter(**kwargs).count()
129
130     def percentage(self, **kwargs):
131         total = self.count()
132         if total == 0:
133             return '-'
134         return '%.2f%%' % (100. * self.count(**kwargs) / total)
135
136     def instru(self):
137         return self.count(instru=True)
138
139     def instru_percentage(self):
140         return self.percentage(instru=True)
141
142     def sabam(self):
143         return self.count(sabam=True)
144
145     def sabam_percentage(self):
146         return self.percentage(sabam=True)
147
148     def cfwb(self):
149         return self.count(cfwb=True)
150
151     def cfwb_percentage(self):
152         return self.percentage(cfwb=True)
153
154     def unset_language(self):
155         return self.count(language='')
156
157     def unset_language_percentage(self):
158         return self.percentage(language='')
159
160     def french(self):
161         return self.count(language='fr')
162
163     def unset_or_na_language(self):
164         return self.qs.filter(Q(language='') | Q(language='na')).count()
165
166     def french_percentage(self):
167         considered_tracks = self.count() - self.unset_or_na_language()
168         if considered_tracks == 0:
169             return '-'
170         return '%.2f%%' % (100. * self.french() / considered_tracks)
171
172     def quota_french(self):
173         # obligation de diffuser annuellement au moins 30% d'œuvres musicales de
174         # langue française
175         considered_tracks = self.count() - self.unset_or_na_language()
176         if considered_tracks == 0:
177             return True
178         return (100. * self.french() / considered_tracks) > 30.
179
180     def quota_cfwb(self):
181         # obligation de diffuser annuellement au moins 4,5% d'œuvres musicales
182         # émanant de la Communauté française
183         considered_tracks = self.count()
184         if considered_tracks == 0:
185             return True
186         return (100. * self.cfwb() / considered_tracks) > 4.5
187
188     def new_files(self):
189         return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
190
191     def percent_new_files(self):
192         return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
193
194
195 def parse_date(date):
196     if date.endswith('d'):
197         return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
198     return datetime.datetime.strptime(date, '%Y-%m-%d').date()
199
200 class StatisticsView(TemplateView):
201     template_name = 'nonstop/statistics.html'
202
203     def get_context_data(self, **kwargs):
204         context = super(StatisticsView, self).get_context_data(**kwargs)
205         context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
206         kwargs = {}
207         if 'from' in self.request.GET:
208             kwargs['from_date'] = parse_date(self.request.GET['from'])
209             context['from_date'] = kwargs['from_date']
210         if 'until' in self.request.GET:
211             kwargs['until_date'] = parse_date(self.request.GET['until'])
212         if 'onair' in self.request.GET:
213             kwargs['nonstopfile__somalogline__on_air'] = True
214         for zone in context['zones']:
215             zone.stats = ZoneStats(zone, **kwargs)
216         return context
217
218
219 class UploadTracksView(FormView):
220     form_class = UploadTracksForm
221     template_name = 'nonstop/upload.html'
222     success_url = '.'
223
224     def post(self, request, *args, **kwargs):
225         assert self.request.user.has_perm('nonstop.add_track')
226         form_class = self.get_form_class()
227         form = self.get_form(form_class)
228         tracks = request.FILES.getlist('tracks')
229         if not form.is_valid():
230             return self.form_invalid(form)
231         missing_metadata = []
232         metadatas = {}
233         for f in tracks:
234             with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
235                 tmpfile.write(f.read())
236                 f.seek(0)
237                 metadata = mutagen.File(tmpfile.name, easy=True)
238             if not metadata or not metadata.get('artist') or not metadata.get('title'):
239                 missing_metadata.append(f.name)
240             else:
241                 metadatas[f.name] = metadata
242         if missing_metadata:
243             form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
244             return self.form_invalid(form)
245
246         for f in tracks:
247             metadata = metadatas[f.name]
248             artist_name = metadata.get('artist')[0]
249             track_title = metadata.get('title')[0]
250
251             monthdir = datetime.datetime.today().strftime('%Y-%m')
252             filepath = '%s/%s - %s - %s%s' % (monthdir,
253                 datetime.datetime.today().strftime('%y%m%d'),
254                 artist_name[:50].replace('/', ' ').strip(),
255                 track_title[:80].replace('/', ' ').strip(),
256                 os.path.splitext(f.name)[-1])
257
258             artist, created = Artist.objects.get_or_create(name=artist_name)
259             track, created = Track.objects.get_or_create(title=track_title, artist=artist,
260                     defaults={'uploader': self.request.user})
261             if created or not track.file_exists():
262                 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
263                 nonstop_file = NonstopFile()
264                 nonstop_file.set_track_filepath(filepath)
265                 nonstop_file.track = track
266                 nonstop_file.save()
267             else:
268                 # don't keep duplicated file and do not create a duplicated nonstop file object
269                 pass
270             if request.POST.get('nonstop_zone'):
271                 track.nonstop_zones.add(
272                         Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
273             track.sync_nonstop_zones()
274
275         messages.info(self.request, '%d new track(s)' % len(tracks))
276         return self.form_valid(form)
277
278
279 class TracksMetadataView(ListView):
280     template_name = 'nonstop/tracks_metadata.html'
281
282     def get_context_data(self, **kwargs):
283         context = super().get_context_data(**kwargs)
284         context['view'] = self
285         return context
286
287     def get_queryset(self):
288         return Track.objects.exclude(nonstop_zones__isnull=True)
289
290     def post(self, request, *args, **kwargs):
291         assert self.request.user.has_perm('nonstop.add_track')
292         for track_id in request.POST.getlist('track'):
293             track = Track.objects.get(id=track_id)
294             track.language = request.POST.get('lang-%s' % track_id, '')
295             track.instru = 'instru-%s' % track_id in request.POST
296             track.sabam = 'sabam-%s' % track_id in request.POST
297             track.cfwb = 'cfwb-%s' % track_id in request.POST
298             track.save()
299         return HttpResponseRedirect('.')
300
301
302 class RandomTracksMetadataView(TracksMetadataView):
303     page_title = _('Metadata of random tracks')
304
305     def get_queryset(self):
306         return super().get_queryset().filter(Q(language='') | Q(language__isnull=True)).order_by('?')[:50]
307
308
309 class RecentTracksMetadataView(TracksMetadataView):
310     page_title = _('Metadata of recent tracks')
311
312     def get_queryset(self):
313         return super().get_queryset().exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
314
315
316 class ArtistTracksMetadataView(TracksMetadataView):
317
318     @property
319     def page_title(self):
320         return _('Metadata of tracks from %s') % Artist.objects.get(id=self.kwargs['artist_pk']).name
321
322     def get_queryset(self):
323         return Track.objects.filter(artist_id=self.kwargs['artist_pk']).order_by('title')
324
325
326 class QuickLinksView(TemplateView):
327     template_name = 'nonstop/quick_links.html'
328
329     def get_context_data(self, **kwargs):
330         context = super().get_context_data(**kwargs)
331         day = datetime.datetime.today()
332         context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
333         return context
334
335
336 class SearchView(TemplateView):
337     template_name = 'nonstop/search.html'
338
339     def get_queryset(self):
340         queryset = Track.objects.all()
341
342         q = self.request.GET.get('q')
343         if q:
344             queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
345
346         zone = self.request.GET.get('zone')
347         if zone:
348             from emissions.models import Nonstop
349             if zone == 'none':
350                 queryset = queryset.filter(nonstop_zones=None)
351             elif zone == 'any':
352                 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
353             else:
354                 queryset = queryset.filter(nonstop_zones=zone)
355
356         order = self.request.GET.get('order_by') or 'title'
357         if order:
358             if 'added_to_nonstop_timestamp' in order:
359                 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
360             queryset = queryset.order_by(order)
361         return queryset
362
363     def get_context_data(self, **kwargs):
364         ctx = super(SearchView, self).get_context_data(**kwargs)
365         ctx['form'] = TrackSearchForm(self.request.GET)
366         queryset = self.get_queryset()
367         qs = self.request.GET.copy()
368         qs.pop('page', None)
369         ctx['qs'] = qs.urlencode()
370
371         tracks = Paginator(queryset.select_related(), 20)
372
373         page = self.request.GET.get('page')
374         try:
375             ctx['tracks'] = tracks.page(page)
376         except PageNotAnInteger:
377             ctx['tracks'] = tracks.page(1)
378         except EmptyPage:
379             ctx['tracks'] = tracks.page(tracks.num_pages)
380
381         return ctx
382
383
384 class SearchCsvView(SearchView):
385     def get(self, request, *args, **kwargs):
386         out = StringIO()
387         writer = csv.writer(out)
388         writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
389         for track in self.get_queryset():
390             writer.writerow([
391                 track.title if track.title else 'Inconnu',
392                 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
393                 ' + '.join([x.title for x in track.nonstop_zones.all()]),
394                 track.language or '',
395                 track.instru and 'instru' or '',
396                 track.cfwb and 'cfwb' or '',
397                 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
398                 ])
399         return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
400
401
402 class CleanupView(TemplateView):
403     template_name = 'nonstop/cleanup.html'
404
405     def get_context_data(self, **kwargs):
406         ctx = super(CleanupView, self).get_context_data(**kwargs)
407         ctx['form'] = CleanupForm()
408
409         zone = self.request.GET.get('zone')
410         if zone:
411             from emissions.models import Nonstop
412             ctx['zone'] = Nonstop.objects.get(id=zone)
413             ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
414             ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
415                             'added_to_nonstop_timestamp').select_related()[:30]
416         return ctx
417
418     def post(self, request, *args, **kwargs):
419         assert self.request.user.has_perm('nonstop.add_track')
420         count = 0
421         for track_id in request.POST.getlist('track'):
422             if request.POST.get('remove-%s' % track_id):
423                 track = Track.objects.get(id=track_id)
424                 track.nonstop_zones.clear()
425                 track.sync_nonstop_zones()
426                 count += 1
427         if count:
428             messages.info(self.request, 'Removed %d new track(s)' % count)
429         return HttpResponseRedirect('.')
430
431
432 class AddSomaDiffusionView(CreateView):
433     model = ScheduledDiffusion
434     fields = ['jingle', 'stream']
435     template_name = 'nonstop/streamed-diffusion.html'
436
437     @property
438     def fields(self):
439         diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
440         fields = ['jingle']
441         if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
442             fields.append('stream')
443         return fields
444
445     def get_initial(self):
446         initial = super(AddSomaDiffusionView, self).get_initial()
447         initial['jingle'] = None
448         if 'stream' in self.fields:
449             initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
450         initial['stream'] = Stream.objects.all().first()
451         return initial
452
453     def form_valid(self, form):
454         form.instance.diffusion_id = self.kwargs['pk']
455         episode = form.instance.diffusion.episode
456         if 'stream' in self.fields and form.instance.stream_id is None:
457             messages.error(self.request, _('missing stream'))
458             return HttpResponseRedirect(reverse('episode-view', kwargs={
459                 'emission_slug': episode.emission.slug,
460                 'slug': episode.slug}))
461         response = super(AddSomaDiffusionView, self).form_valid(form)
462         messages.info(self.request, _('%s added to schedule') % episode.emission.title)
463         return response
464
465     def get_success_url(self):
466         diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
467         episode = diffusion.episode
468         return reverse('episode-view', kwargs={
469             'emission_slug': episode.emission.slug,
470             'slug': episode.slug})
471
472
473 class DelSomaDiffusionView(RedirectView):
474     def get_redirect_url(self, pk):
475         soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
476         episode = soma_diffusion.episode
477         ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
478         messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
479         return reverse('episode-view', kwargs={
480             'emission_slug': episode.emission.slug,
481             'slug': episode.slug})
482
483
484 class DiffusionPropertiesView(UpdateView):
485     model = ScheduledDiffusion
486     fields = ['jingle', 'stream']
487     template_name = 'nonstop/streamed-diffusion.html'
488
489     @property
490     def fields(self):
491         diffusion = self.get_object().diffusion
492         fields = ['jingle']
493         if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
494             fields.append('stream')
495         return fields
496
497     def form_valid(self, form):
498         episode = self.get_object().diffusion.episode
499         if 'stream' in self.fields and form.instance.stream_id is None:
500             messages.error(self.request, _('missing stream'))
501             return HttpResponseRedirect(reverse('episode-view', kwargs={
502                 'emission_slug': episode.emission.slug,
503                 'slug': episode.slug}))
504         response = super(DiffusionPropertiesView, self).form_valid(form)
505         messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
506         return response
507
508     def get_success_url(self):
509         episode = self.get_object().diffusion.episode
510         return reverse('episode-view', kwargs={
511             'emission_slug': episode.emission.slug,
512             'slug': episode.slug})
513
514
515 def jingle_audio_view(request, *args, **kwargs):
516     jingle = Jingle.objects.get(id=kwargs['pk'])
517     return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
518
519
520 class AjaxProgram(TemplateView):
521     template_name = 'nonstop/program-fragment.html'
522
523     def get_context_data(self, date, **kwargs):
524         context = super().get_context_data(**kwargs)
525         now = datetime.datetime.now()
526         if date:
527             date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
528         else:
529             date_start = datetime.datetime.today()
530         date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
531         today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
532         date_end = date_start + datetime.timedelta(days=1)
533         context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
534         for x in context['day_program']:
535             x.klass = x.__class__.__name__
536         previous_prog = None
537         for i, x in enumerate(context['day_program']):
538             if today and x.datetime > now and previous_prog:
539                 previous_prog.now = True
540                 break
541             previous_prog = x
542         return context
543
544
545 class ZoneSettings(FormView):
546     form_class = ZoneSettingsForm
547     template_name = 'nonstop/zone_settings.html'
548     success_url = reverse_lazy('nonstop-quick-links')
549
550     def get_context_data(self, **kwargs):
551         context = super().get_context_data(**kwargs)
552         context['zone'] = Nonstop.objects.get(slug=self.kwargs['slug'])
553         return context
554
555     def get_initial(self):
556         try:
557             zone = Nonstop.objects.get(slug=self.kwargs['slug'])
558         except Nonstop.DoesNotExist:
559             raise Http404()
560         zone_settings = zone.nonstopzonesettings_set.first()
561         if zone_settings is None:
562             zone_settings = NonstopZoneSettings(nonstop=zone)
563             zone_settings.save()
564         initial = super().get_initial()
565         initial['start'] = zone.start.strftime('%H:%M') if zone.start else None
566         initial['end'] = zone.end.strftime('%H:%M') if zone.end else None
567         initial['intro_jingle'] = zone_settings.intro_jingle_id
568         initial['jingles'] = [x.id for x in zone_settings.jingles.all()]
569         return initial
570
571     def form_valid(self, form):
572         zone = Nonstop.objects.get(slug=self.kwargs['slug'])
573         zone_settings = zone.nonstopzonesettings_set.first()
574         zone.start = form.cleaned_data['start']
575         zone.end = form.cleaned_data['end']
576         zone_settings.jingles.set(form.cleaned_data['jingles'])
577         zone_settings.intro_jingle_id = form.cleaned_data['intro_jingle']
578         zone.save()
579         zone_settings.save()
580         return super().form_valid(form)