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 QuickLinksView(TemplateView):
317 template_name = 'nonstop/quick_links.html'
319 def get_context_data(self, **kwargs):
320 context = super().get_context_data(**kwargs)
321 day = datetime.datetime.today()
322 context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
326 class SearchView(TemplateView):
327 template_name = 'nonstop/search.html'
329 def get_queryset(self):
330 queryset = Track.objects.all()
332 q = self.request.GET.get('q')
334 queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
336 zone = self.request.GET.get('zone')
338 from emissions.models import Nonstop
340 queryset = queryset.filter(nonstop_zones=None)
342 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
344 queryset = queryset.filter(nonstop_zones=zone)
346 order = self.request.GET.get('order_by') or 'title'
348 if 'added_to_nonstop_timestamp' in order:
349 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
350 queryset = queryset.order_by(order)
353 def get_context_data(self, **kwargs):
354 ctx = super(SearchView, self).get_context_data(**kwargs)
355 ctx['form'] = TrackSearchForm(self.request.GET)
356 queryset = self.get_queryset()
357 qs = self.request.GET.copy()
359 ctx['qs'] = qs.urlencode()
361 tracks = Paginator(queryset.select_related(), 20)
363 page = self.request.GET.get('page')
365 ctx['tracks'] = tracks.page(page)
366 except PageNotAnInteger:
367 ctx['tracks'] = tracks.page(1)
369 ctx['tracks'] = tracks.page(tracks.num_pages)
374 class SearchCsvView(SearchView):
375 def get(self, request, *args, **kwargs):
377 writer = csv.writer(out)
378 writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
379 for track in self.get_queryset():
381 track.title if track.title else 'Inconnu',
382 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
383 ' + '.join([x.title for x in track.nonstop_zones.all()]),
384 track.language or '',
385 track.instru and 'instru' or '',
386 track.cfwb and 'cfwb' or '',
387 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
389 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
392 class CleanupView(TemplateView):
393 template_name = 'nonstop/cleanup.html'
395 def get_context_data(self, **kwargs):
396 ctx = super(CleanupView, self).get_context_data(**kwargs)
397 ctx['form'] = CleanupForm()
399 zone = self.request.GET.get('zone')
401 from emissions.models import Nonstop
402 ctx['zone'] = Nonstop.objects.get(id=zone)
403 ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
404 ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
405 'added_to_nonstop_timestamp').select_related()[:30]
408 def post(self, request, *args, **kwargs):
409 assert self.request.user.has_perm('nonstop.add_track')
411 for track_id in request.POST.getlist('track'):
412 if request.POST.get('remove-%s' % track_id):
413 track = Track.objects.get(id=track_id)
414 track.nonstop_zones.clear()
415 track.sync_nonstop_zones()
418 messages.info(self.request, 'Removed %d new track(s)' % count)
419 return HttpResponseRedirect('.')
422 class AddSomaDiffusionView(CreateView):
423 model = ScheduledDiffusion
424 fields = ['jingle', 'stream']
425 template_name = 'nonstop/streamed-diffusion.html'
429 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
431 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
432 fields.append('stream')
435 def get_initial(self):
436 initial = super(AddSomaDiffusionView, self).get_initial()
437 initial['jingle'] = None
438 if 'stream' in self.fields:
439 initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
440 initial['stream'] = Stream.objects.all().first()
443 def form_valid(self, form):
444 form.instance.diffusion_id = self.kwargs['pk']
445 episode = form.instance.diffusion.episode
446 if 'stream' in self.fields and form.instance.stream_id is None:
447 messages.error(self.request, _('missing stream'))
448 return HttpResponseRedirect(reverse('episode-view', kwargs={
449 'emission_slug': episode.emission.slug,
450 'slug': episode.slug}))
451 response = super(AddSomaDiffusionView, self).form_valid(form)
452 messages.info(self.request, _('%s added to schedule') % episode.emission.title)
455 def get_success_url(self):
456 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
457 episode = diffusion.episode
458 return reverse('episode-view', kwargs={
459 'emission_slug': episode.emission.slug,
460 'slug': episode.slug})
463 class DelSomaDiffusionView(RedirectView):
464 def get_redirect_url(self, pk):
465 soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
466 episode = soma_diffusion.episode
467 ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
468 messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
469 return reverse('episode-view', kwargs={
470 'emission_slug': episode.emission.slug,
471 'slug': episode.slug})
474 class DiffusionPropertiesView(UpdateView):
475 model = ScheduledDiffusion
476 fields = ['jingle', 'stream']
477 template_name = 'nonstop/streamed-diffusion.html'
481 diffusion = self.get_object().diffusion
483 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
484 fields.append('stream')
487 def form_valid(self, form):
488 episode = self.get_object().diffusion.episode
489 if 'stream' in self.fields and form.instance.stream_id is None:
490 messages.error(self.request, _('missing stream'))
491 return HttpResponseRedirect(reverse('episode-view', kwargs={
492 'emission_slug': episode.emission.slug,
493 'slug': episode.slug}))
494 response = super(DiffusionPropertiesView, self).form_valid(form)
495 messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
498 def get_success_url(self):
499 episode = self.get_object().diffusion.episode
500 return reverse('episode-view', kwargs={
501 'emission_slug': episode.emission.slug,
502 'slug': episode.slug})
505 def jingle_audio_view(request, *args, **kwargs):
506 jingle = Jingle.objects.get(id=kwargs['pk'])
507 return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
510 class AjaxProgram(TemplateView):
511 template_name = 'nonstop/program-fragment.html'
513 def get_context_data(self, date, **kwargs):
514 context = super().get_context_data(**kwargs)
515 now = datetime.datetime.now()
517 date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
519 date_start = datetime.datetime.today()
520 date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
521 today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
522 date_end = date_start + datetime.timedelta(days=1)
523 context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
524 for x in context['day_program']:
525 x.klass = x.__class__.__name__
527 for i, x in enumerate(context['day_program']):
528 if today and x.datetime > now and previous_prog:
529 previous_prog.now = True
535 class ZoneSettings(FormView):
536 form_class = ZoneSettingsForm
537 template_name = 'nonstop/zone_settings.html'
538 success_url = reverse_lazy('nonstop-quick-links')
540 def get_context_data(self, **kwargs):
541 context = super().get_context_data(**kwargs)
542 context['zone'] = Nonstop.objects.get(slug=self.kwargs['slug'])
545 def get_initial(self):
547 zone = Nonstop.objects.get(slug=self.kwargs['slug'])
548 except Nonstop.DoesNotExist:
550 zone_settings = zone.nonstopzonesettings_set.first()
551 if zone_settings is None:
552 zone_settings = NonstopZoneSettings(nonstop=zone)
554 initial = super().get_initial()
555 initial['start'] = zone.start.strftime('%H:%M') if zone.start else None
556 initial['end'] = zone.end.strftime('%H:%M') if zone.end else None
557 initial['intro_jingle'] = zone_settings.intro_jingle_id
558 initial['jingles'] = [x.id for x in zone_settings.jingles.all()]
561 def form_valid(self, form):
562 zone = Nonstop.objects.get(slug=self.kwargs['slug'])
563 zone_settings = zone.nonstopzonesettings_set.first()
564 zone.start = form.cleaned_data['start']
565 zone.end = form.cleaned_data['end']
566 zone_settings.jingles.set(form.cleaned_data['jingles'])
567 zone_settings.intro_jingle_id = form.cleaned_data['intro_jingle']
570 return super().form_valid(form)