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):
110 total = self.qs.filter(**kwargs).aggregate(Sum('duration'))['duration__sum'].total_seconds()
112 duration = _('%d hours') % (total / 3600)
114 duration = _('%d minutes') % (total / 60)
115 start = datetime.datetime(2000, 1, 1, self.zone.start.hour, self.zone.start.minute)
116 end = datetime.datetime(2000, 1, 1, self.zone.end.hour, self.zone.end.minute)
118 end = end + datetime.timedelta(days=1)
119 return duration + _(', → %d days') % (total // (end - start).total_seconds())
121 def count(self, **kwargs):
122 return self.qs.filter(**kwargs).count()
124 def percentage(self, **kwargs):
128 return '%.2f%%' % (100. * self.count(**kwargs) / total)
131 return self.count(instru=True)
133 def instru_percentage(self):
134 return self.percentage(instru=True)
137 return self.count(sabam=True)
139 def sabam_percentage(self):
140 return self.percentage(sabam=True)
143 return self.count(cfwb=True)
145 def cfwb_percentage(self):
146 return self.percentage(cfwb=True)
149 return self.count(language='fr')
151 def french_percentage(self):
152 considered_tracks = self.count() - self.instru()
153 if considered_tracks == 0:
155 return '%.2f%%' % (100. * self.french() / considered_tracks)
158 return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
160 def percent_new_files(self):
161 return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
164 def parse_date(date):
165 if date.endswith('d'):
166 return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
167 return datetime.datetime.strptime(date, '%Y-%m-%d').date()
169 class StatisticsView(TemplateView):
170 template_name = 'nonstop/statistics.html'
172 def get_context_data(self, **kwargs):
173 context = super(StatisticsView, self).get_context_data(**kwargs)
174 context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
176 if 'from' in self.request.GET:
177 kwargs['from_date'] = parse_date(self.request.GET['from'])
178 context['from_date'] = kwargs['from_date']
179 if 'until' in self.request.GET:
180 kwargs['until_date'] = parse_date(self.request.GET['until'])
181 if 'onair' in self.request.GET:
182 kwargs['nonstopfile__somalogline__on_air'] = True
183 for zone in context['zones']:
184 zone.stats = ZoneStats(zone, **kwargs)
188 class UploadTracksView(FormView):
189 form_class = UploadTracksForm
190 template_name = 'nonstop/upload.html'
193 def post(self, request, *args, **kwargs):
194 assert self.request.user.has_perm('nonstop.add_track')
195 form_class = self.get_form_class()
196 form = self.get_form(form_class)
197 tracks = request.FILES.getlist('tracks')
198 if not form.is_valid():
199 return self.form_invalid(form)
200 missing_metadata = []
203 with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
204 tmpfile.write(f.read())
206 metadata = mutagen.File(tmpfile.name, easy=True)
207 if not metadata or not metadata.get('artist') or not metadata.get('title'):
208 missing_metadata.append(f.name)
210 metadatas[f.name] = metadata
212 form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
213 return self.form_invalid(form)
216 metadata = metadatas[f.name]
217 artist_name = metadata.get('artist')[0]
218 track_title = metadata.get('title')[0]
220 monthdir = datetime.datetime.today().strftime('%Y-%m')
221 filepath = '%s/%s - %s - %s%s' % (monthdir,
222 datetime.datetime.today().strftime('%y%m%d'),
223 artist_name[:50].replace('/', ' ').strip(),
224 track_title[:80].replace('/', ' ').strip(),
225 os.path.splitext(f.name)[-1])
227 artist, created = Artist.objects.get_or_create(name=artist_name)
228 track, created = Track.objects.get_or_create(title=track_title, artist=artist,
229 defaults={'uploader': self.request.user})
230 if created or not track.file_exists():
231 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
232 nonstop_file = NonstopFile()
233 nonstop_file.set_track_filepath(filepath)
234 nonstop_file.track = track
237 # don't keep duplicated file and do not create a duplicated nonstop file object
239 if request.POST.get('nonstop_zone'):
240 track.nonstop_zones.add(
241 Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
242 track.sync_nonstop_zones()
244 messages.info(self.request, '%d new track(s)' % len(tracks))
245 return self.form_valid(form)
248 class RecentTracksView(ListView):
249 template_name = 'nonstop/recent_tracks.html'
251 def get_queryset(self):
252 return Track.objects.exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
254 def post(self, request, *args, **kwargs):
255 assert self.request.user.has_perm('nonstop.add_track')
256 for track_id in request.POST.getlist('track'):
257 track = Track.objects.get(id=track_id)
258 track.language = request.POST.get('lang-%s' % track_id, '')
259 track.instru = 'instru-%s' % track_id in request.POST
260 track.sabam = 'sabam-%s' % track_id in request.POST
261 track.cfwb = 'cfwb-%s' % track_id in request.POST
263 return HttpResponseRedirect('.')
266 class QuickLinksView(TemplateView):
267 template_name = 'nonstop/quick_links.html'
269 def get_context_data(self, **kwargs):
270 context = super().get_context_data(**kwargs)
271 day = datetime.datetime.today()
272 context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
276 class SearchView(TemplateView):
277 template_name = 'nonstop/search.html'
279 def get_queryset(self):
280 queryset = Track.objects.all()
282 q = self.request.GET.get('q')
284 queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
286 zone = self.request.GET.get('zone')
288 from emissions.models import Nonstop
290 queryset = queryset.filter(nonstop_zones=None)
292 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
294 queryset = queryset.filter(nonstop_zones=zone)
296 order = self.request.GET.get('order_by') or 'title'
298 if 'added_to_nonstop_timestamp' in order:
299 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
300 queryset = queryset.order_by(order)
303 def get_context_data(self, **kwargs):
304 ctx = super(SearchView, self).get_context_data(**kwargs)
305 ctx['form'] = TrackSearchForm(self.request.GET)
306 queryset = self.get_queryset()
307 qs = self.request.GET.copy()
309 ctx['qs'] = qs.urlencode()
311 tracks = Paginator(queryset.select_related(), 20)
313 page = self.request.GET.get('page')
315 ctx['tracks'] = tracks.page(page)
316 except PageNotAnInteger:
317 ctx['tracks'] = tracks.page(1)
319 ctx['tracks'] = tracks.page(tracks.num_pages)
324 class SearchCsvView(SearchView):
325 def get(self, request, *args, **kwargs):
327 writer = csv.writer(out)
328 writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
329 for track in self.get_queryset():
331 track.title if track.title else 'Inconnu',
332 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
333 ' + '.join([x.title for x in track.nonstop_zones.all()]),
334 track.language or '',
335 track.instru and 'instru' or '',
336 track.cfwb and 'cfwb' or '',
337 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
339 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
342 class CleanupView(TemplateView):
343 template_name = 'nonstop/cleanup.html'
345 def get_context_data(self, **kwargs):
346 ctx = super(CleanupView, self).get_context_data(**kwargs)
347 ctx['form'] = CleanupForm()
349 zone = self.request.GET.get('zone')
351 from emissions.models import Nonstop
352 ctx['zone'] = Nonstop.objects.get(id=zone)
353 ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
354 ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
355 'added_to_nonstop_timestamp').select_related()[:30]
358 def post(self, request, *args, **kwargs):
359 assert self.request.user.has_perm('nonstop.add_track')
361 for track_id in request.POST.getlist('track'):
362 if request.POST.get('remove-%s' % track_id):
363 track = Track.objects.get(id=track_id)
364 track.nonstop_zones.clear()
365 track.sync_nonstop_zones()
368 messages.info(self.request, 'Removed %d new track(s)' % count)
369 return HttpResponseRedirect('.')
372 class AddSomaDiffusionView(CreateView):
373 model = ScheduledDiffusion
374 fields = ['jingle', 'stream']
375 template_name = 'nonstop/streamed-diffusion.html'
379 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
381 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
382 fields.append('stream')
385 def get_initial(self):
386 initial = super(AddSomaDiffusionView, self).get_initial()
387 initial['jingle'] = None
388 if 'stream' in self.fields:
389 initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
390 initial['stream'] = Stream.objects.all().first()
393 def form_valid(self, form):
394 form.instance.diffusion_id = self.kwargs['pk']
395 episode = form.instance.diffusion.episode
396 if 'stream' in self.fields and form.instance.stream_id is None:
397 messages.error(self.request, _('missing stream'))
398 return HttpResponseRedirect(reverse('episode-view', kwargs={
399 'emission_slug': episode.emission.slug,
400 'slug': episode.slug}))
401 response = super(AddSomaDiffusionView, self).form_valid(form)
402 messages.info(self.request, _('%s added to schedule') % episode.emission.title)
405 def get_success_url(self):
406 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
407 episode = diffusion.episode
408 return reverse('episode-view', kwargs={
409 'emission_slug': episode.emission.slug,
410 'slug': episode.slug})
413 class DelSomaDiffusionView(RedirectView):
414 def get_redirect_url(self, pk):
415 soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
416 episode = soma_diffusion.episode
417 ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
418 messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
419 return reverse('episode-view', kwargs={
420 'emission_slug': episode.emission.slug,
421 'slug': episode.slug})
424 class DiffusionPropertiesView(UpdateView):
425 model = ScheduledDiffusion
426 fields = ['jingle', 'stream']
427 template_name = 'nonstop/streamed-diffusion.html'
431 diffusion = self.get_object().diffusion
433 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
434 fields.append('stream')
437 def form_valid(self, form):
438 episode = self.get_object().diffusion.episode
439 if 'stream' in self.fields and form.instance.stream_id is None:
440 messages.error(self.request, _('missing stream'))
441 return HttpResponseRedirect(reverse('episode-view', kwargs={
442 'emission_slug': episode.emission.slug,
443 'slug': episode.slug}))
444 response = super(DiffusionPropertiesView, self).form_valid(form)
445 messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
448 def get_success_url(self):
449 episode = self.get_object().diffusion.episode
450 return reverse('episode-view', kwargs={
451 'emission_slug': episode.emission.slug,
452 'slug': episode.slug})
455 def jingle_audio_view(request, *args, **kwargs):
456 jingle = Jingle.objects.get(id=kwargs['pk'])
457 return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
460 class AjaxProgram(TemplateView):
461 template_name = 'nonstop/program-fragment.html'
463 def get_context_data(self, date, **kwargs):
464 context = super().get_context_data(**kwargs)
465 now = datetime.datetime.now()
467 date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
469 date_start = datetime.datetime.today()
470 date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
471 today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
472 date_end = date_start + datetime.timedelta(days=1)
473 context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
474 for x in context['day_program']:
475 x.klass = x.__class__.__name__
477 for i, x in enumerate(context['day_program']):
478 if today and x.datetime > now and previous_prog:
479 previous_prog.now = True