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
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 count(self, **kwargs):
110 return self.qs.filter(**kwargs).count()
112 def percentage(self, **kwargs):
116 return '%.2f%%' % (100. * self.count(**kwargs) / total)
119 return self.count(instru=True)
121 def instru_percentage(self):
122 return self.percentage(instru=True)
125 return self.count(sabam=True)
127 def sabam_percentage(self):
128 return self.percentage(sabam=True)
131 return self.count(cfwb=True)
133 def cfwb_percentage(self):
134 return self.percentage(cfwb=True)
137 return self.count(language='fr')
139 def french_percentage(self):
140 considered_tracks = self.count() - self.instru()
141 if considered_tracks == 0:
143 return '%.2f%%' % (100. * self.french() / considered_tracks)
146 return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
148 def percent_new_files(self):
149 return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
152 def parse_date(date):
153 if date.endswith('d'):
154 return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
155 return datetime.datetime.strptime(date, '%Y-%m-%d').date()
157 class StatisticsView(TemplateView):
158 template_name = 'nonstop/statistics.html'
160 def get_context_data(self, **kwargs):
161 context = super(StatisticsView, self).get_context_data(**kwargs)
162 context['zones'] = [x for x in Nonstop.objects.all().order_by('start') if x.start != x.end]
164 if 'from' in self.request.GET:
165 kwargs['from_date'] = parse_date(self.request.GET['from'])
166 context['from_date'] = kwargs['from_date']
167 if 'until' in self.request.GET:
168 kwargs['until_date'] = parse_date(self.request.GET['until'])
169 if 'onair' in self.request.GET:
170 kwargs['nonstopfile__somalogline__on_air'] = True
171 for zone in context['zones']:
172 zone.stats = ZoneStats(zone, **kwargs)
176 class UploadTracksView(FormView):
177 form_class = UploadTracksForm
178 template_name = 'nonstop/upload.html'
181 def post(self, request, *args, **kwargs):
182 assert self.request.user.has_perm('nonstop.add_track')
183 form_class = self.get_form_class()
184 form = self.get_form(form_class)
185 tracks = request.FILES.getlist('tracks')
186 if not form.is_valid():
187 return self.form_invalid(form)
188 missing_metadata = []
191 with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
192 tmpfile.write(f.read())
194 metadata = mutagen.File(tmpfile.name, easy=True)
195 if not metadata or not metadata.get('artist') or not metadata.get('title'):
196 missing_metadata.append(f.name)
198 metadatas[f.name] = metadata
200 form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
201 return self.form_invalid(form)
204 metadata = metadatas[f.name]
205 artist_name = metadata.get('artist')[0]
206 track_title = metadata.get('title')[0]
208 monthdir = datetime.datetime.today().strftime('%Y-%m')
209 filepath = '%s/%s - %s - %s%s' % (monthdir,
210 datetime.datetime.today().strftime('%y%m%d'),
211 artist_name[:50].replace('/', ' ').strip(),
212 track_title[:80].replace('/', ' ').strip(),
213 os.path.splitext(f.name)[-1])
215 artist, created = Artist.objects.get_or_create(name=artist_name)
216 track, created = Track.objects.get_or_create(title=track_title, artist=artist,
217 defaults={'uploader': self.request.user})
218 if created or not track.file_exists():
219 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
220 nonstop_file = NonstopFile()
221 nonstop_file.set_track_filepath(filepath)
222 nonstop_file.track = track
225 # don't keep duplicated file and do not create a duplicated nonstop file object
227 if request.POST.get('nonstop_zone'):
228 track.nonstop_zones.add(
229 Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
230 track.sync_nonstop_zones()
232 messages.info(self.request, '%d new track(s)' % len(tracks))
233 return self.form_valid(form)
236 class RecentTracksView(ListView):
237 template_name = 'nonstop/recent_tracks.html'
239 def get_queryset(self):
240 return Track.objects.exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
242 def post(self, request, *args, **kwargs):
243 assert self.request.user.has_perm('nonstop.add_track')
244 for track_id in request.POST.getlist('track'):
245 track = Track.objects.get(id=track_id)
246 track.language = request.POST.get('lang-%s' % track_id, '')
247 track.instru = 'instru-%s' % track_id in request.POST
248 track.sabam = 'sabam-%s' % track_id in request.POST
249 track.cfwb = 'cfwb-%s' % track_id in request.POST
251 return HttpResponseRedirect('.')
254 class QuickLinksView(TemplateView):
255 template_name = 'nonstop/quick_links.html'
257 def get_context_data(self, **kwargs):
258 context = super().get_context_data(**kwargs)
259 day = datetime.datetime.today()
260 context['days'] = [day + datetime.timedelta(days=i) for i in range(5)]
264 class SearchView(TemplateView):
265 template_name = 'nonstop/search.html'
267 def get_queryset(self):
268 queryset = Track.objects.all()
270 q = self.request.GET.get('q')
272 queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
274 zone = self.request.GET.get('zone')
276 from emissions.models import Nonstop
278 queryset = queryset.filter(nonstop_zones=None)
280 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
282 queryset = queryset.filter(nonstop_zones=zone)
284 order = self.request.GET.get('order_by') or 'title'
286 if 'added_to_nonstop_timestamp' in order:
287 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
288 queryset = queryset.order_by(order)
291 def get_context_data(self, **kwargs):
292 ctx = super(SearchView, self).get_context_data(**kwargs)
293 ctx['form'] = TrackSearchForm(self.request.GET)
294 queryset = self.get_queryset()
295 qs = self.request.GET.copy()
297 ctx['qs'] = qs.urlencode()
299 tracks = Paginator(queryset.select_related(), 20)
301 page = self.request.GET.get('page')
303 ctx['tracks'] = tracks.page(page)
304 except PageNotAnInteger:
305 ctx['tracks'] = tracks.page(1)
307 ctx['tracks'] = tracks.page(tracks.num_pages)
312 class SearchCsvView(SearchView):
313 def get(self, request, *args, **kwargs):
315 writer = csv.writer(out)
316 writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
317 for track in self.get_queryset():
319 track.title if track.title else 'Inconnu',
320 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
321 ' + '.join([x.title for x in track.nonstop_zones.all()]),
322 track.language or '',
323 track.instru and 'instru' or '',
324 track.cfwb and 'cfwb' or '',
325 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
327 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
330 class CleanupView(TemplateView):
331 template_name = 'nonstop/cleanup.html'
333 def get_context_data(self, **kwargs):
334 ctx = super(CleanupView, self).get_context_data(**kwargs)
335 ctx['form'] = CleanupForm()
337 zone = self.request.GET.get('zone')
339 from emissions.models import Nonstop
340 ctx['zone'] = Nonstop.objects.get(id=zone)
341 ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
342 ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
343 'added_to_nonstop_timestamp').select_related()[:30]
346 def post(self, request, *args, **kwargs):
347 assert self.request.user.has_perm('nonstop.add_track')
349 for track_id in request.POST.getlist('track'):
350 if request.POST.get('remove-%s' % track_id):
351 track = Track.objects.get(id=track_id)
352 track.nonstop_zones.clear()
353 track.sync_nonstop_zones()
356 messages.info(self.request, 'Removed %d new track(s)' % count)
357 return HttpResponseRedirect('.')
360 class AddSomaDiffusionView(CreateView):
361 model = ScheduledDiffusion
362 fields = ['jingle', 'stream']
363 template_name = 'nonstop/streamed-diffusion.html'
367 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
369 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
370 fields.append('stream')
373 def get_initial(self):
374 initial = super(AddSomaDiffusionView, self).get_initial()
375 initial['jingle'] = None
376 if 'stream' in self.fields:
377 initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
378 initial['stream'] = Stream.objects.all().first()
381 def form_valid(self, form):
382 form.instance.diffusion_id = self.kwargs['pk']
383 episode = form.instance.diffusion.episode
384 if 'stream' in self.fields and form.instance.stream_id is None:
385 messages.error(self.request, _('missing stream'))
386 return HttpResponseRedirect(reverse('episode-view', kwargs={
387 'emission_slug': episode.emission.slug,
388 'slug': episode.slug}))
389 response = super(AddSomaDiffusionView, self).form_valid(form)
390 messages.info(self.request, _('%s added to schedule') % episode.emission.title)
393 def get_success_url(self):
394 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
395 episode = diffusion.episode
396 return reverse('episode-view', kwargs={
397 'emission_slug': episode.emission.slug,
398 'slug': episode.slug})
401 class DelSomaDiffusionView(RedirectView):
402 def get_redirect_url(self, pk):
403 soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
404 episode = soma_diffusion.episode
405 ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
406 messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
407 return reverse('episode-view', kwargs={
408 'emission_slug': episode.emission.slug,
409 'slug': episode.slug})
412 class DiffusionPropertiesView(UpdateView):
413 model = ScheduledDiffusion
414 fields = ['jingle', 'stream']
415 template_name = 'nonstop/streamed-diffusion.html'
419 diffusion = self.get_object().diffusion
421 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
422 fields.append('stream')
425 def form_valid(self, form):
426 episode = self.get_object().diffusion.episode
427 if 'stream' in self.fields and form.instance.stream_id is None:
428 messages.error(self.request, _('missing stream'))
429 return HttpResponseRedirect(reverse('episode-view', kwargs={
430 'emission_slug': episode.emission.slug,
431 'slug': episode.slug}))
432 response = super(DiffusionPropertiesView, self).form_valid(form)
433 messages.info(self.request, _('%s diffusion properties have been updated.') % episode.emission.title)
436 def get_success_url(self):
437 episode = self.get_object().diffusion.episode
438 return reverse('episode-view', kwargs={
439 'emission_slug': episode.emission.slug,
440 'slug': episode.slug})
443 def jingle_audio_view(request, *args, **kwargs):
444 jingle = Jingle.objects.get(id=kwargs['pk'])
445 return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))
448 class AjaxProgram(TemplateView):
449 template_name = 'nonstop/program-fragment.html'
451 def get_context_data(self, date, **kwargs):
452 context = super().get_context_data(**kwargs)
453 now = datetime.datetime.now()
455 date_start = datetime.datetime.strptime(date, '%Y-%m-%d')
457 date_start = datetime.datetime.today()
458 date_start = date_start.replace(hour=5, minute=0, second=0, microsecond=0)
459 today = bool(date_start.timetuple()[:3] == now.timetuple()[:3])
460 date_end = date_start + datetime.timedelta(days=1)
461 context['day_program'] = period_program(date_start, date_end, prefetch_categories=False)
462 for x in context['day_program']:
463 x.klass = x.__class__.__name__
465 for i, x in enumerate(context['day_program']):
466 if today and x.datetime > now and previous_prog:
467 previous_prog.now = True