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 unset_language(self):
155 return self.count(language='')
157 def unset_language_percentage(self):
158 return self.percentage(language='')
161 return self.count(language='fr')
163 def unset_or_na_language(self):
164 return self.qs.filter(Q(language='') | Q(language='na')).count()
166 def french_percentage(self):
167 considered_tracks = self.count() - self.unset_or_na_language()
168 if considered_tracks == 0:
170 return '%.2f%%' % (100. * self.french() / considered_tracks)
172 def quota_french(self):
173 # obligation de diffuser annuellement au moins 30% d'œuvres musicales de
175 considered_tracks = self.count() - self.unset_or_na_language()
176 if considered_tracks == 0:
178 return (100. * self.french() / considered_tracks) > 30.
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:
186 return (100. * self.cfwb() / considered_tracks) > 4.5
189 return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
191 def percent_new_files(self):
192 return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
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()
200 class StatisticsView(TemplateView):
201 template_name = 'nonstop/statistics.html'
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]
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)
219 class UploadTracksView(FormView):
220 form_class = UploadTracksForm
221 template_name = 'nonstop/upload.html'
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 = []
234 with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
235 tmpfile.write(f.read())
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)
241 metadatas[f.name] = metadata
243 form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
244 return self.form_invalid(form)
247 metadata = metadatas[f.name]
248 artist_name = metadata.get('artist')[0]
249 track_title = metadata.get('title')[0]
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])
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
268 # don't keep duplicated file and do not create a duplicated nonstop file object
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()
275 messages.info(self.request, '%d new track(s)' % len(tracks))
276 return self.form_valid(form)
279 class TracksMetadataView(ListView):
280 template_name = 'nonstop/tracks_metadata.html'
282 def get_context_data(self, **kwargs):
283 context = super().get_context_data(**kwargs)
284 context['view'] = self
287 def get_queryset(self):
288 return Track.objects.exclude(nonstop_zones__isnull=True)
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
299 return HttpResponseRedirect('.')
302 class RandomTracksMetadataView(TracksMetadataView):
303 page_title = _('Metadata of random tracks')
305 def get_queryset(self):
306 return super().get_queryset().filter(Q(language='') | Q(language__isnull=True)).order_by('?')[:50]
309 class RecentTracksMetadataView(TracksMetadataView):
310 page_title = _('Metadata of recent tracks')
312 def get_queryset(self):
313 return super().get_queryset().exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
316 class ArtistTracksMetadataView(TracksMetadataView):
319 def page_title(self):
320 return _('Metadata of tracks from %s') % Artist.objects.get(id=self.kwargs['artist_pk']).name
322 def get_queryset(self):
323 return Track.objects.filter(artist_id=self.kwargs['artist_pk']).order_by('title')
326 class QuickLinksView(TemplateView):
327 template_name = 'nonstop/quick_links.html'
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)]
336 class SearchView(TemplateView):
337 template_name = 'nonstop/search.html'
339 def get_queryset(self):
340 queryset = Track.objects.all()
342 q = self.request.GET.get('q')
344 queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
346 zone = self.request.GET.get('zone')
348 from emissions.models import Nonstop
350 queryset = queryset.filter(nonstop_zones=None)
352 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
354 queryset = queryset.filter(nonstop_zones=zone)
356 order = self.request.GET.get('order_by') or 'title'
358 if 'added_to_nonstop_timestamp' in order:
359 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
360 queryset = queryset.order_by(order)
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()
369 ctx['qs'] = qs.urlencode()
371 tracks = Paginator(queryset.select_related(), 20)
373 page = self.request.GET.get('page')
375 ctx['tracks'] = tracks.page(page)
376 except PageNotAnInteger:
377 ctx['tracks'] = tracks.page(1)
379 ctx['tracks'] = tracks.page(tracks.num_pages)
384 class SearchCsvView(SearchView):
385 def get(self, request, *args, **kwargs):
387 writer = csv.writer(out)
388 writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
389 for track in self.get_queryset():
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 '',
399 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
402 class CleanupView(TemplateView):
403 template_name = 'nonstop/cleanup.html'
405 def get_context_data(self, **kwargs):
406 ctx = super(CleanupView, self).get_context_data(**kwargs)
407 ctx['form'] = CleanupForm()
409 zone = self.request.GET.get('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]
418 def post(self, request, *args, **kwargs):
419 assert self.request.user.has_perm('nonstop.add_track')
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()
428 messages.info(self.request, 'Removed %d new track(s)' % count)
429 return HttpResponseRedirect('.')
432 class AddSomaDiffusionView(CreateView):
433 model = ScheduledDiffusion
434 fields = ['jingle', 'stream']
435 template_name = 'nonstop/streamed-diffusion.html'
439 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
441 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
442 fields.append('stream')
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()
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)
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})
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})
484 class DiffusionPropertiesView(UpdateView):
485 model = ScheduledDiffusion
486 fields = ['jingle', 'stream']
487 template_name = 'nonstop/streamed-diffusion.html'
491 diffusion = self.get_object().diffusion
493 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
494 fields.append('stream')
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)
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})
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'))
520 class AjaxProgram(TemplateView):
521 template_name = 'nonstop/program-fragment.html'
523 def get_context_data(self, date, **kwargs):
524 context = super().get_context_data(**kwargs)
525 now = datetime.datetime.now()
527 date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
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__
537 for i, x in enumerate(context['day_program']):
538 if today and x.datetime > now and previous_prog:
539 previous_prog.now = True
545 class ZoneSettings(FormView):
546 form_class = ZoneSettingsForm
547 template_name = 'nonstop/zone_settings.html'
548 success_url = reverse_lazy('nonstop-quick-links')
550 def get_context_data(self, **kwargs):
551 context = super().get_context_data(**kwargs)
552 context['zone'] = Nonstop.objects.get(slug=self.kwargs['slug'])
555 def get_initial(self):
557 zone = Nonstop.objects.get(slug=self.kwargs['slug'])
558 except Nonstop.DoesNotExist:
560 zone_settings = zone.nonstopzonesettings_set.first()
561 if zone_settings is None:
562 zone_settings = NonstopZoneSettings(nonstop=zone)
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()]
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']
580 return super().form_valid(form)