Off by one error.
[audio.git] / src / archive / views.py
1 from datetime import datetime
2 import os
3 import os.path
4 from urllib.parse import quote
5
6 from archive import settings
7 from django.contrib.auth.decorators import permission_required
8 from django.contrib.postgres.search import SearchVector
9 from django.urls import reverse
10 from django.db.models import Q, Max
11 from django.http import Http404, HttpResponse
12 from django.shortcuts import render, redirect, get_object_or_404
13 from django.utils.translation import gettext as _
14 from django.views.decorators.http import require_POST
15 from django.views.generic import ListView
16
17 import mutagen
18
19 from archive.constants import status
20 from archive import models
21 from archive.forms import AudiobookForm
22 from archive import tasks
23 from archive.utils import all_files
24
25
26 def list_new(request):
27     path = settings.NEW_PATH
28     objects = sorted(all_files(path))
29     return render(request, "archive/list_new.html", locals())
30
31
32 @permission_required('archive.change_audiobook')
33 def file_new(request, filename):
34     filepath = filename
35     root_filepath = os.path.join(settings.NEW_PATH, filename)
36     if request.POST:
37         form = AudiobookForm(request.POST)
38         if form.is_valid():
39             try:
40                 form.save(path=filepath)
41             except IOError:
42                 raise Http404
43             return redirect(list_new)
44
45     try:
46         tags = mutagen.File(root_filepath)
47     except IOError:
48         raise Http404
49     d = {}
50     if tags:
51         for tag in tags:
52             value = tags[tag]
53             if isinstance(value, list):
54                 d[tag] = value[0]
55             else:
56                 d[tag] = value
57             if tag == 'project':
58                 try:
59                     d[tag] = models.Project.objects.get(name=d[tag]).pk
60                 except models.Project.DoesNotExist:
61                     d[tag] = None
62
63     if not request.POST:
64         form = AudiobookForm(initial=d)
65     return render(request, "archive/file_new.html", locals())
66
67
68 @require_POST
69 @permission_required('archive.change_audiobook')
70 def move_to_archive(request, filename):
71     """ move a new file to the unmanaged files dir """
72
73     old_path = os.path.join(settings.NEW_PATH, filename)
74     new_path = os.path.join(settings.UNMANAGED_PATH, filename)
75     new_dir = os.path.split(new_path)[0]
76     if not os.path.isdir(new_dir):
77         os.makedirs(new_dir)
78
79     if not os.path.isfile(old_path):
80         raise Http404
81
82     try:
83         os.link(old_path, new_path)
84         os.unlink(old_path)
85     except OSError:
86         # destination file exists, don't overwrite it
87         # TODO: this should probably be more informative
88         return redirect(file_new, filename)
89
90     return redirect(list_new)
91
92
93 @require_POST
94 @permission_required('archive.change_audiobook')
95 def remove_to_archive(request, aid):
96     """ move a managed file to the unmanaged files dir """
97
98     audiobook = get_object_or_404(models.Audiobook, id=aid)
99     old_path = audiobook.source_file.path
100     new_path = os.path.join(settings.UNMANAGED_PATH,
101         str(audiobook.source_file)[len(settings.FILES_SAVE_PATH):].lstrip('/'))
102     new_dir = os.path.split(new_path)[0]
103     if not os.path.isdir(new_dir):
104         os.makedirs(new_dir)
105
106     if not os.path.isfile(old_path):
107         raise Http404
108
109     success = False
110     try_new_path = new_path
111     try_number = 0
112     while not success:
113         try:
114             os.link(old_path, try_new_path)
115         except OSError:
116             # destination file exists, don't overwrite it
117             try_number += 1
118             parts = new_path.rsplit('.', 1)
119             parts[0] += '_%d' % try_number
120             try_new_path = ".".join(parts)
121         else:
122             os.unlink(old_path)
123             audiobook.delete()
124             success = True
125
126     return redirect(list_unmanaged)
127
128 @require_POST
129 @permission_required('archive.change_audiobook')
130 def move_to_new(request, filename):
131     """ move a unmanaged file to new files dir """
132
133     old_path = os.path.join(settings.UNMANAGED_PATH, filename)
134     new_path = os.path.join(settings.NEW_PATH, filename)
135     new_dir = os.path.split(new_path)[0]
136     if not os.path.isdir(new_dir):
137         os.makedirs(new_dir)
138
139     if not os.path.isfile(old_path):
140         raise Http404
141
142     try:
143         os.link(old_path, new_path)
144         os.unlink(old_path)
145     except OSError:
146         # destination file exists, don't overwrite it
147         # TODO: this should probably be more informative
148         return redirect(reverse(file_unmanaged, args=[filename]) + "?exists=1")
149
150     return redirect(list_unmanaged)
151
152
153 @require_POST
154 @permission_required('archive.change_audiobook')
155 def publish(request, aid, publish=True):
156     """ mark file for publishing """
157     audiobook = get_object_or_404(models.Audiobook, id=aid)
158     audiobook.publish(request.user, publish=publish)
159     return redirect(file_managed, aid)
160
161
162 @require_POST
163 @permission_required('archive.change_audiobook')
164 def cancel_publishing(request, aid):
165     """ cancel scheduled publishing """
166     audiobook = get_object_or_404(models.Audiobook, id=aid)
167     # TODO: cancel tasks
168     audiobook.mp3_status = None
169     audiobook.ogg_status = None
170     audiobook.youtube_status = None
171     audiobook.youtube_queued = None
172     audiobook.save()
173     return redirect(file_managed, aid)
174
175
176 def download(request, aid, which="source"):
177     if which not in ("source", "mp3", "ogg", 'mkv'):
178         raise Http404
179     audiobook = get_object_or_404(models.Audiobook, id=aid)
180     field = which
181     if which == 'mkv':
182         field = 'youtube'
183     file_ = getattr(audiobook, "%s_file" % field)
184     if not file_:
185         raise Http404
186     ext = file_.path.rsplit('.', 1)[-1]
187     response = HttpResponse(content_type='application/force-download')
188     
189     response['Content-Disposition'] = "attachment; filename*=UTF-8''%s.%s" % (
190         quote(audiobook.title.encode('utf-8'), safe=''), ext)
191     with open(file_.path, 'rb') as f:
192         response.write(f.read())
193     #response['X-Sendfile'] = file_.path.encode('utf-8')
194     return response
195
196
197 def list_publishing(request):
198     objects = models.Audiobook.objects.exclude(
199         mp3_status=None, ogg_status=None, youtube_status=None
200     ).order_by("youtube_queued", "title")
201     objects_by_status = {}
202     for o in objects:
203         statuses = set()
204         if o.mp3_status:
205             statuses.add((o.mp3_status, o.get_mp3_status_display()))
206         if o.ogg_status:
207             statuses.add((o.ogg_status, o.get_ogg_status_display()))
208         if o.youtube_status:
209             statuses.add((o.youtube_status, o.get_youtube_status_display()))
210         for status in statuses:
211             objects_by_status.setdefault(status, []).append(o)
212     status_objects = sorted(objects_by_status.items(), reverse=True)
213
214     return render(request, "archive/list_publishing.html", locals())
215
216
217 class AudiobookList(ListView):
218     def get_queryset(self):
219         qs = models.Audiobook.objects.all()
220         if 's' in self.request.GET:
221             qs = qs.annotate(s=SearchVector('title', 'slug')).filter(s=self.request.GET['s'])
222         return qs
223
224
225 @permission_required('archive.change_audiobook')
226 def file_managed(request, id):
227     audiobook = get_object_or_404(models.Audiobook, id=id)
228
229     if request.POST:
230         form = AudiobookForm(request.POST, instance=audiobook)
231         if form.is_valid():
232             try:
233                 form.save()
234             except IOError:
235                 raise Http404
236
237     tags = {}
238     if audiobook.source_file:
239         path = audiobook.source_file.path[len(settings.FILES_PATH):].lstrip('/')
240
241         # for tags update
242         tags = mutagen.File(audiobook.source_file.path.encode('utf-8'))
243         if not tags:
244             tags = {}
245     form = AudiobookForm(instance=audiobook)
246
247     user_can_publish = (
248         request.user.is_authenticated and
249         request.user.oauthconnection_set.filter(access=True).exists())
250
251     alerts = []
252     parts_count = audiobook.parts_count
253     if parts_count > 1:
254         series = models.Audiobook.objects.filter(slug=audiobook.slug)
255         if not audiobook.index:
256             alerts.append(_('There is more than one part, but index is not set.'))
257         if set(series.values_list('index', flat=True)) != set(range(1, parts_count + 1)):
258             alerts.append(_('Part indexes are not 1..%(parts_count)d.') % {"parts_count": parts_count})
259
260     from youtube.models import YouTube
261     youtube = YouTube.objects.first()
262     youtube_title = youtube.get_title(audiobook)
263     youtube_description = youtube.get_description(audiobook)
264
265             
266     return render(request, "archive/file_managed.html", locals())
267
268
269 def list_unmanaged(request):
270     objects = sorted(all_files(settings.UNMANAGED_PATH))
271     return render(request, "archive/list_unmanaged.html", locals())
272
273
274 def file_unmanaged(request, filename):
275     tags = mutagen.File(os.path.join(settings.UNMANAGED_PATH, filename))
276     if not tags:
277         tags = {}
278     
279     err_exists = request.GET.get('exists')
280     return render(request, "archive/file_unmanaged.html", locals())
281
282
283 class BookView(ListView):
284     template_name = 'archive/book.html'
285
286     def get_queryset(self):
287         qs = models.Audiobook.objects.filter(slug=self.kwargs["slug"]).order_by(
288             "index"
289         )
290         last_vol = None
291         last_vol_sub = None
292         for b in qs:
293             if last_vol is None or last_vol.youtube_volume_index != b.youtube_volume_index:
294                 last_vol = b
295                 b.total = 0
296                 if last_vol_sub is None or b.youtube_volume:
297                     last_vol_sub = last_vol
298                     last_vol_sub.total_for_sub = 0
299             last_vol.total += b.duration
300             last_vol_sub.total_for_sub += b.duration
301             b.subtotal = last_vol_sub.total_for_sub
302         return list(qs)
303
304
305 @permission_required('archive.change_audiobook')
306 def book_youtube_volume(request, aid):
307     audiobook = get_object_or_404(models.Audiobook, id=aid)
308     slug = audiobook.slug
309     cur_vol = audiobook.youtube_volume
310     new_vol = request.POST.get('volume', '')
311
312     audiobook.youtube_volume = new_vol
313     audiobook.save()
314     
315     for a in models.Audiobook.objects.filter(slug=slug, youtube_volume=cur_vol, index__gt=audiobook.index).order_by('index'):
316         if a.youtube_volume != cur_vol:
317             break
318         a.youtube_volume = new_vol
319         a.save()
320     
321     return redirect('book', audiobook.slug)
322