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
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
28 from .app_settings import app_settings
31 class SomaDayArchiveView(DayArchiveView):
32 queryset = SomaLogLine.objects.all()
33 date_field = "play_timestamp"
34 make_object_list = True
39 class SomaDayArchiveCsvView(SomaDayArchiveView):
40 def render_to_response(self, context, **response_kwargs):
42 writer = csv.writer(out)
43 for line in context['object_list']:
44 if line.filepath.track:
45 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
47 line.filepath.track.title,
48 line.filepath.track.artist.name,
49 line.filepath.track.language,
50 line.filepath.track.instru and 'instru' or '',
51 line.filepath.track.cfwb and 'cfwb' or '',
52 line.filepath.track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if line.filepath.track.added_to_nonstop_timestamp else '',
55 writer.writerow([line.play_timestamp.strftime('%Y-%m-%d %H:%M'),
57 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
60 class RedirectTodayView(RedirectView):
61 def get_redirect_url(self, *args, **kwargs):
62 today = datetime.datetime.today()
63 return reverse('archive_day', kwargs={
69 class TrackDetailView(DetailView):
72 def get_context_data(self, **kwargs):
73 ctx = super(TrackDetailView, self).get_context_data(**kwargs)
74 ctx['metadata_form'] = TrackMetaForm(instance=self.object)
77 def post(self, request, *args, **kwargs):
78 assert self.request.user.has_perm('nonstop.add_track')
79 instance = self.get_object()
80 old_nonstop_zones = copy.copy(instance.nonstop_zones.all())
81 form = TrackMetaForm(request.POST, instance=instance)
83 new_nonstop_zones = self.get_object().nonstop_zones.all()
84 if set(old_nonstop_zones) != set(new_nonstop_zones):
85 instance.sync_nonstop_zones()
86 return HttpResponseRedirect('.')
89 class ArtistDetailView(DetailView):
93 class ArtistListView(ListView):
97 class ZoneStats(object):
98 def __init__(self, zone, from_date=None, until_date=None, **kwargs):
100 self.qs = Track.objects.filter(nonstop_zones=self.zone, **kwargs)
101 self.from_date = from_date
103 self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__gte=from_date)
105 self.qs = self.qs.filter(nonstopfile__somalogline__play_timestamp__lte=until_date)
106 self.qs = self.qs.distinct()
108 def count(self, **kwargs):
109 return self.qs.filter(**kwargs).count()
111 def percentage(self, **kwargs):
115 return '%.2f%%' % (100. * self.count(**kwargs) / total)
118 return self.count(instru=True)
120 def instru_percentage(self):
121 return self.percentage(instru=True)
124 return self.count(sabam=True)
126 def sabam_percentage(self):
127 return self.percentage(sabam=True)
130 return self.count(cfwb=True)
132 def cfwb_percentage(self):
133 return self.percentage(cfwb=True)
136 return self.count(language='fr')
138 def french_percentage(self):
139 considered_tracks = self.count() - self.instru()
140 if considered_tracks == 0:
142 return '%.2f%%' % (100. * self.french() / considered_tracks)
145 return self.count(nonstopfile__creation_timestamp__gte=self.from_date)
147 def percent_new_files(self):
148 return self.percentage(nonstopfile__creation_timestamp__gte=self.from_date)
151 def parse_date(date):
152 if date.endswith('d'):
153 return datetime.datetime.today() + datetime.timedelta(int(date.rstrip('d')))
154 return datetime.datetime.strptime(date, '%Y-%m-%d').date()
156 class StatisticsView(TemplateView):
157 template_name = 'nonstop/statistics.html'
159 def get_context_data(self, **kwargs):
160 context = super(StatisticsView, self).get_context_data(**kwargs)
161 context['zones'] = Nonstop.objects.all().order_by('start')
163 if 'from' in self.request.GET:
164 kwargs['from_date'] = parse_date(self.request.GET['from'])
165 context['from_date'] = kwargs['from_date']
166 if 'until' in self.request.GET:
167 kwargs['until_date'] = parse_date(self.request.GET['until'])
168 if 'onair' in self.request.GET:
169 kwargs['nonstopfile__somalogline__on_air'] = True
170 for zone in context['zones']:
171 zone.stats = ZoneStats(zone, **kwargs)
175 class UploadTracksView(FormView):
176 form_class = UploadTracksForm
177 template_name = 'nonstop/upload.html'
180 def post(self, request, *args, **kwargs):
181 assert self.request.user.has_perm('nonstop.add_track')
182 form_class = self.get_form_class()
183 form = self.get_form(form_class)
184 tracks = request.FILES.getlist('tracks')
185 if not form.is_valid():
186 return self.form_invalid(form)
187 missing_metadata = []
190 with tempfile.NamedTemporaryFile(prefix='track-upload') as tmpfile:
191 tmpfile.write(f.read())
193 metadata = mutagen.File(tmpfile.name, easy=True)
194 if not metadata or not metadata.get('artist') or not metadata.get('title'):
195 missing_metadata.append(f.name)
197 metadatas[f.name] = metadata
199 form.add_error('tracks', _('Missing metadata in: ') + ', '.join(missing_metadata))
200 return self.form_invalid(form)
203 metadata = metadatas[f.name]
204 artist_name = metadata.get('artist')[0]
205 track_title = metadata.get('title')[0]
207 monthdir = datetime.datetime.today().strftime('%Y-%m')
208 filepath = '%s/%s - %s - %s%s' % (monthdir,
209 datetime.datetime.today().strftime('%y%m%d'),
210 artist_name[:50].replace('/', ' ').strip(),
211 track_title[:80].replace('/', ' ').strip(),
212 os.path.splitext(f.name)[-1])
214 artist, created = Artist.objects.get_or_create(name=artist_name)
215 track, created = Track.objects.get_or_create(title=track_title, artist=artist,
216 defaults={'uploader': self.request.user})
217 if created or not track.file_exists():
218 default_storage.save(os.path.join('nonstop', 'tracks', filepath), content=f)
219 nonstop_file = NonstopFile()
220 nonstop_file.set_track_filepath(filepath)
221 nonstop_file.track = track
224 # don't keep duplicated file and do not create a duplicated nonstop file object
226 if request.POST.get('nonstop_zone'):
227 track.nonstop_zones.add(
228 Nonstop.objects.get(id=request.POST.get('nonstop_zone')))
229 track.sync_nonstop_zones()
231 messages.info(self.request, '%d new track(s)' % len(tracks))
232 return self.form_valid(form)
235 class RecentTracksView(ListView):
236 template_name = 'nonstop/recent_tracks.html'
238 def get_queryset(self):
239 return Track.objects.exclude(creation_timestamp__isnull=True).order_by('-creation_timestamp')[:50]
241 def post(self, request, *args, **kwargs):
242 assert self.request.user.has_perm('nonstop.add_track')
243 for track_id in request.POST.getlist('track'):
244 track = Track.objects.get(id=track_id)
245 track.language = request.POST.get('lang-%s' % track_id, '')
246 track.instru = 'instru-%s' % track_id in request.POST
247 track.sabam = 'sabam-%s' % track_id in request.POST
248 track.cfwb = 'cfwb-%s' % track_id in request.POST
250 return HttpResponseRedirect('.')
253 class QuickLinksView(TemplateView):
254 template_name = 'nonstop/quick_links.html'
257 class SearchView(TemplateView):
258 template_name = 'nonstop/search.html'
260 def get_queryset(self):
261 queryset = Track.objects.all()
263 q = self.request.GET.get('q')
265 queryset = queryset.filter(Q(title__icontains=q.lower()) | Q(artist__name__icontains=q.lower()))
267 zone = self.request.GET.get('zone')
269 from emissions.models import Nonstop
271 queryset = queryset.filter(nonstop_zones=None)
273 queryset = queryset.filter(nonstop_zones__isnull=False).distinct()
275 queryset = queryset.filter(nonstop_zones=zone)
277 order = self.request.GET.get('order_by') or 'title'
279 if 'added_to_nonstop_timestamp' in order:
280 queryset = queryset.filter(added_to_nonstop_timestamp__isnull=False)
281 queryset = queryset.order_by(order)
284 def get_context_data(self, **kwargs):
285 ctx = super(SearchView, self).get_context_data(**kwargs)
286 ctx['form'] = TrackSearchForm(self.request.GET)
287 queryset = self.get_queryset()
288 qs = self.request.GET.copy()
290 ctx['qs'] = qs.urlencode()
292 tracks = Paginator(queryset.select_related(), 20)
294 page = self.request.GET.get('page')
296 ctx['tracks'] = tracks.page(page)
297 except PageNotAnInteger:
298 ctx['tracks'] = tracks.page(1)
300 ctx['tracks'] = tracks.page(tracks.num_pages)
305 class SearchCsvView(SearchView):
306 def get(self, request, *args, **kwargs):
308 writer = csv.writer(out)
309 writer.writerow(['Title', 'Artist', 'Zones', 'Language', 'Instru', 'CFWB', 'Ajout'])
310 for track in self.get_queryset():
312 track.title if track.title else 'Inconnu',
313 track.artist.name if (track.artist and track.artist.name) else 'Inconnu',
314 ' + '.join([x.title for x in track.nonstop_zones.all()]),
315 track.language or '',
316 track.instru and 'instru' or '',
317 track.cfwb and 'cfwb' or '',
318 track.added_to_nonstop_timestamp.strftime('%Y-%m-%d %H:%M') if track.added_to_nonstop_timestamp else '',
320 return HttpResponse(out.getvalue(), content_type='text/csv; charset=utf-8')
323 class CleanupView(TemplateView):
324 template_name = 'nonstop/cleanup.html'
326 def get_context_data(self, **kwargs):
327 ctx = super(CleanupView, self).get_context_data(**kwargs)
328 ctx['form'] = CleanupForm()
330 zone = self.request.GET.get('zone')
332 from emissions.models import Nonstop
333 ctx['zone'] = Nonstop.objects.get(id=zone)
334 ctx['count'] = Track.objects.filter(nonstop_zones=zone).count()
335 ctx['tracks'] = Track.objects.filter(nonstop_zones=zone).order_by(
336 'added_to_nonstop_timestamp').select_related()[:30]
339 def post(self, request, *args, **kwargs):
340 assert self.request.user.has_perm('nonstop.add_track')
342 for track_id in request.POST.getlist('track'):
343 if request.POST.get('remove-%s' % track_id):
344 track = Track.objects.get(id=track_id)
345 track.nonstop_zones.clear()
346 track.sync_nonstop_zones()
349 messages.info(self.request, 'Removed %d new track(s)' % count)
350 return HttpResponseRedirect('.')
353 class AddSomaDiffusionView(CreateView):
354 model = ScheduledDiffusion
355 fields = ['jingle', 'stream']
356 template_name = 'nonstop/streamed-diffusion.html'
360 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
362 if not diffusion.episode.soundfile_set.filter(fragment=False).exists():
363 fields.append('stream')
366 def get_initial(self):
367 initial = super(AddSomaDiffusionView, self).get_initial()
368 initial['jingle'] = None
369 if 'stream' in self.fields:
370 initial['jingle'] = Jingle.objects.filter(default_for_streams=True).first()
371 initial['stream'] = Stream.objects.all().first()
374 def form_valid(self, form):
375 form.instance.diffusion_id = self.kwargs['pk']
376 episode = form.instance.diffusion.episode
377 if 'stream' in self.fields and form.instance.stream_id is None:
378 messages.error(self.request, _('missing stream'))
379 return HttpResponseRedirect(reverse('episode-view', kwargs={
380 'emission_slug': episode.emission.slug,
381 'slug': episode.slug}))
382 response = super(AddSomaDiffusionView, self).form_valid(form)
383 messages.info(self.request, _('%s added to schedule') % episode.emission.title)
386 def get_success_url(self):
387 diffusion = Diffusion.objects.get(id=self.kwargs['pk'])
388 episode = diffusion.episode
389 return reverse('episode-view', kwargs={
390 'emission_slug': episode.emission.slug,
391 'slug': episode.slug})
394 class DelSomaDiffusionView(RedirectView):
395 def get_redirect_url(self, pk):
396 soma_diffusion = ScheduledDiffusion.objects.filter(diffusion_id=pk).first()
397 episode = soma_diffusion.episode
398 ScheduledDiffusion.objects.filter(diffusion_id=pk).update(diffusion_id=None)
399 messages.info(self.request, _('%s removed from schedule') % episode.emission.title)
400 return reverse('episode-view', kwargs={
401 'emission_slug': episode.emission.slug,
402 'slug': episode.slug})
405 def jingle_audio_view(request, *args, **kwargs):
406 jingle = Jingle.objects.get(id=kwargs['pk'])
407 return FileResponse(open(os.path.join(app_settings.LOCAL_BASE_PATH, app_settings.JINGLES_PREFIX, jingle.filepath), 'rb'))