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