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
12 from django.contrib import messages
13 from django.db.models import Q, Sum
14 from django.http import HttpResponse, HttpResponseRedirect, FileResponse
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
24 from .models import (SomaLogLine, Track, Artist, NonstopFile, ScheduledDiffusion, Jingle, Stream)
25 from emissions.models import Nonstop, Diffusion
26 from emissions.utils import period_program
29 from .app_settings import app_settings
32 class SomaDayArchiveView(DayArchiveView):
33 queryset = SomaLogLine.objects.all()
34 date_field = "play_timestamp"
35 make_object_list = True
40 class SomaDayArchiveCsvView(SomaDayArchiveView):
41 def render_to_response(self, context, **response_kwargs):
43 writer = csv.writer(out)
44 for line in context['object_list']:
45 if line.filepath.track:
46 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
48 line.filepath.track.title,
49 line.filepath.track.artist.name,
50 line.filepath.track.language,
51 line.filepath.track.instru and 'instru' or '',
52 line.filepath.track.cfwb and 'cfwb' or '',
53 line.filepath.track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if line.filepath.track.added_to_nonstop_timestamp else '',
56 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
58 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
61 class RedirectTodayView(RedirectView):
62 def get_redirect_url(self, *args, **kwargs):
63 today = datetime.datetime.today()
64 return reverse('archive_day', kwargs={
70 class TrackDetailView(DetailView):
73 def get_context_data(self, **kwargs):
74 ctx = super(TrackDetailView, self).get_context_data(**kwargs)
75 ctx['metadata_form'] = TrackMetaForm(instance=self.object)
78 def post(self, request, *args, **kwargs):
79 assert self.request.user.has_perm('nonstop.add_track')
80 instance = self.get_object()
81 old_nonstop_zones = copy.copy(instance.nonstop_zones.all())
82 form = TrackMetaForm(request.POST, instance=instance)
84 new_nonstop_zones = self.get_object().nonstop_zones.all()
85 if set(old_nonstop_zones) != set(new_nonstop_zones):
86 instance.sync_nonstop_zones()
87 return HttpResponseRedirect('.')
90 class ArtistDetailView(DetailView):
94 class ArtistListView(ListView):
98 class ZoneStats(object):
99 def __init__(self, zone, from_date=None, until_date=None, **kwargs):
101 self.qs = Track.objects.filter(nonstop_zones=self.zone, **kwargs)
102 self.from_date = from_date
104 self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__gte=from_date)
106 self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__lte=until_date)
107 self.qs = self.qs.distinct()
109 def total_duration(self, **kwargs):
111 total = self.qs.filter(**kwargs).aggregate(Sum('duration'))['duration__sum'].total_seconds()
112 except AttributeError:
113 # 'NoneType' object has no attribute 'total_seconds', if there's no
117 duration = _('%d hours') % (total / 3600)
119 duration = _('%d minutes') % (total / 60)
120 start = datetime.datetime(2000, 1, 1, self.zone.start.hour, self.zone.start.minute)
121 end = datetime.datetime(2000, 1, 1, self.zone.end.hour, self.zone.end.minute)
123 end = end + datetime.timedelta(days=1)
124 return duration + _(', → %d days') % (total // (end - start).total_seconds())
126 def count(self, **kwargs):
127 return self.qs.filter(**kwargs).count()
129 def percentage(self, **kwargs):
133 return '%.2f%%' % (100. * self.count(**kwargs) / total)
136 return self.count(instru=True)
138 def instru_percentage(self):
139 return self.percentage(instru=True)
142 return self.count(sabam=True)
144 def sabam_percentage(self):
145 return self.percentage(sabam=True)
148 return self.count(cfwb=True)
150 def cfwb_percentage(self):
151 return self.percentage(cfwb=True)
154 return self.count(language='fr')
156 def french_percentage(self):
157 considered_tracks = self.count() - self.instru()
158 if considered_tracks == 0:
160 return '%.2f%%' % (100. * self.french() / considered_tracks)
162 def quota_french(self):
163 # obligation de diffuser annuellement au moins 30% d'œuvres musicales de
165 considered_tracks = self.count() - self.instru()
166 if considered_tracks == 0:
168 return (100. * self.french() / considered_tracks) > 30.
170 def quota_cfwb(self):
171 # obligation de diffuser annuellement au moins 4,5% d'œuvres musicales
172 # émanant de la Communauté française
173 considered_tracks = self.count()
174 if considered_tracks == 0:
176 return (100. * self.cfwb() / considered_tracks) > 4.5
179 return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
181 def percent_new_files(self):
182 return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
185 def parse_date(date):
186 if date.endswith('d'):
187 return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
188 return datetime.datetime.strptime(date, '%Y-%m-%d').date()
190 class StatisticsView(TemplateView):
191 template_name = 'nonstop/statistics.html'
193 def get_context_data(self, **kwargs):
194 context = super(StatisticsView, self).get_context_data(**kwargs)
195 context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
197 if 'from' in self.request.GET:
198 kwargs['from_date'] = parse_date(self.request.GET['from'])
199 context['from_date'] = kwargs['from_date']
200 if 'until' in self.request.GET:
201 kwargs['until_date'] = parse_date(self.request.GET['until'])
202 if 'onair' in self.request.GET:
203 kwargs['nonstopfile__somalogline__on_air'] = True
204 for zone in context['zones']:
205 zone.stats = ZoneStats(zone, **kwargs)
209 class UploadTracksView(FormView):
210 form_class = UploadTracksForm
211 template_name = 'nonstop/upload.html'
214 def post(self, request, *args, **kwargs):
215 assert self.request.user.has_perm('nonstop.add_track')
216 form_class = self.get_form_class()
217 form = self.get_form(form_class)
218 tracks = request.FILES.getlist('tracks')
219 if not form.is_valid():
220 return self.form_invalid(form)
221 missing_metadata = []
224 with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
225 tmpfile.write(f.read())
227 metadata = mutagen.File(tmpfile.name, easy=True)
228 if not metadata or not metadata.get('artist') or not metadata.get('title'):
229 missing_metadata.append(f.name)
231 metadatas[f.name] = metadata
233 form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
234 return self.form_invalid(form)
237 metadata = metadatas[f.name]
238 artist_name = metadata.get('artist')[0]
239 track_title = metadata.get('title')[0]
241 monthdir = datetime.datetime.today().strftime('%Y-%m')
242 filepath = '%s/%s - %s - %s%s' % (monthdir,
243 datetime.datetime.today().strftime('%y%m%d'),
244 artist_name[:50].replace('/', ' ').strip(),
245 track_title[:80].replace('/', ' ').strip(),
246 os.path.splitext(f.name)[-1])
248 artist, created = Artist.objects.get_or_create(name=artist_name)
249 track, created = Track.objects.get_or_create(title=track_title, artist=artist,
250 defaults={'uploader': self.request.user})
251 if created or not track.file_exists():
252 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
253 nonstop_file = NonstopFile()
254 nonstop_file.set_track_filepath(filepath)
255 nonstop_file.track = track
258 # don't keep duplicated file and do not create a duplicated nonstop file object
260 if request.POST.get('nonstop_zone'):
261 track.nonstop_zones.add(
262 Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
263 track.sync_nonstop_zones()
265 messages.info(self.request, '%d new track(s)' % len(tracks))
266 return self.form_valid(form)
269 class RecentTracksView(ListView):
270 template_name = 'nonstop/recent_tracks.html'
272 def get_queryset(self):
273 return Track.objects.exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
275 def post(self, request, *args, **kwargs):
276 assert self.request.user.has_perm('nonstop.add_track')
277 for track_id in request.POST.getlist('track'):
278 track = Track.objects.get(id=track_id)
279 track.language = request.POST.get('lang-%s' % track_id, '')
280 track.instru = 'instru-%s' % track_id in request.POST
281 track.sabam = 'sabam-%s' % track_id in request.POST
282 track.cfwb = 'cfwb-%s' % track_id in request.POST
284 return HttpResponseRedirect('.')
287 class QuickLinksView(TemplateView):
288 template_name = 'nonstop/quick_links.html'
290 def get_context_data(self, **kwargs):
291 context = super().get_context_data(**kwargs)
292 day = datetime.datetime.today()
293 context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
297 class SearchView(TemplateView):
298 template_name = 'nonstop/search.html'
300 def get_queryset(self):
301 queryset = Track.objects.all()
303 q = self.request.GET.get('q')
305 queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
307 zone = self.request.GET.get('zone')
309 from emissions.models import Nonstop
311 queryset = queryset.filter(nonstop_zones=None)
313 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
315 queryset = queryset.filter(nonstop_zones=zone)
317 order = self.request.GET.get('order_by') or 'title'
319 if 'added_to_nonstop_timestamp' in order:
320 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
321 queryset = queryset.order_by(order)
324 def get_context_data(self, **kwargs):
325 ctx = super(SearchView, self).get_context_data(**kwargs)
326 ctx['form'] = TrackSearchForm(self.request.GET)
327 queryset = self.get_queryset()
328 qs = self.request.GET.copy()
330 ctx['qs'] = qs.urlencode()
332 tracks = Paginator(queryset.select_related(), 20)
334 page = self.request.GET.get('page')
336 ctx['tracks'] = tracks.page(page)
337 except PageNotAnInteger:
338 ctx['tracks'] = tracks.page(1)
340 ctx['tracks'] = tracks.page(tracks.num_pages)
345 class SearchCsvView(SearchView):
346 def get(self, request, *args, **kwargs):
348 writer = csv.writer(out)
349 writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
350 for track in self.get_queryset():
352 track.title if track.title else 'Inconnu',
353 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
354 ' + '.join([x.title for x in track.nonstop_zones.all()]),
355 track.language or '',
356 track.instru and 'instru' or '',
357 track.cfwb and 'cfwb' or '',
358 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
360 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
363 class CleanupView(TemplateView):
364 template_name = 'nonstop/cleanup.html'
366 def get_context_data(self, **kwargs):
367 ctx = super(CleanupView, self).get_context_data(**kwargs)
368 ctx['form'] = CleanupForm()
370 zone = self.request.GET.get('zone')
372 from emissions.models import Nonstop
373 ctx['zone'] = Nonstop.objects.get(id=zone)
374 ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
375 ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
376 'added_to_nonstop_timestamp').select_related()[:30]
379 def post(self, request, *args, **kwargs):
380 assert self.request.user.has_perm('nonstop.add_track')
382 for track_id in request.POST.getlist('track'):
383 if request.POST.get('remove-%s' % track_id):
384 track = Track.objects.get(id=track_id)
385 track.nonstop_zones.clear()
386 track.sync_nonstop_zones()
389 messages.info(self.request, 'Removed %d new track(s)' % count)
390 return HttpResponseRedirect('.')
393 class AddSomaDiffusionView(CreateView):
394 model = ScheduledDiffusion
395 fields = ['jingle', 'stream']
396 template_name = 'nonstop/streamed-diffusion.html'
400 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
402 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
403 fields.append('stream')
406 def get_initial(self):
407 initial = super(AddSomaDiffusionView, self).get_initial()
408 initial['jingle'] = None
409 if 'stream' in self.fields:
410 initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
411 initial['stream'] = Stream.objects.all().first()
414 def form_valid(self, form):
415 form.instance.diffusion_id = self.kwargs['pk']
416 episode = form.instance.diffusion.episode
417 if 'stream' in self.fields and form.instance.stream_id is None:
418 messages.error(self.request, _('missing stream'))
419 return HttpResponseRedirect(reverse('episode-view', kwargs={
420 'emission_slug': episode.emission.slug,
421 'slug': episode.slug}))
422 response = super(AddSomaDiffusionView, self).form_valid(form)
423 messages.info(self.request, _('%s added to schedule') % episode.emission.title)
426 def get_success_url(self):
427 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
428 episode = diffusion.episode
429 return reverse('episode-view', kwargs={
430 'emission_slug': episode.emission.slug,
431 'slug': episode.slug})
434 class DelSomaDiffusionView(RedirectView):
435 def get_redirect_url(self, pk):
436 soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
437 episode = soma_diffusion.episode
438 ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
439 messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
440 return reverse('episode-view', kwargs={
441 'emission_slug': episode.emission.slug,
442 'slug': episode.slug})
445 class DiffusionPropertiesView(UpdateView):
446 model = ScheduledDiffusion
447 fields = ['jingle', 'stream']
448 template_name = 'nonstop/streamed-diffusion.html'
452 diffusion = self.get_object().diffusion
454 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
455 fields.append('stream')
458 def form_valid(self, form):
459 episode = self.get_object().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(DiffusionPropertiesView, self).form_valid(form)
466 messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
469 def get_success_url(self):
470 episode = self.get_object().diffusion.episode
471 return reverse('episode-view', kwargs={
472 'emission_slug': episode.emission.slug,
473 'slug': episode.slug})
476 def jingle_audio_view(request, *args, **kwargs):
477 jingle = Jingle.objects.get(id=kwargs['pk'])
478 return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
481 class AjaxProgram(TemplateView):
482 template_name = 'nonstop/program-fragment.html'
484 def get_context_data(self, date, **kwargs):
485 context = super().get_context_data(**kwargs)
486 now = datetime.datetime.now()
488 date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
490 date_start = datetime.datetime.today()
491 date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
492 today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
493 date_end = date_start + datetime.timedelta(days=1)
494 context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
495 for x in context['day_program']:
496 x.klass = x.__class__.__name__
498 for i, x in enumerate(context['day_program']):
499 if today and x.datetime > now and previous_prog:
500 previous_prog.now = True