48652037bfc0e2a6b7b8e1e20b921634d24eaa29
[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.publish(request.user, publish=publish)
161     return redirect(file_managed, aid)
162
163
164 @require_POST
165 @permission_required('archive.change_audiobook')
166 def cancel_publishing(request, aid):
167     """ cancel scheduled publishing """
168     audiobook = get_object_or_404(models.Audiobook, id=aid)
169     # TODO: cancel tasks
170     audiobook.mp3_status = None
171     audiobook.ogg_status = None
172     audiobook.youtube_status = None
173     audiobook.youtube_queued = None
174     audiobook.save()
175     return redirect(file_managed, aid)
176
177
178 def download(request, aid, which="source"):
179     if which not in ("source", "mp3", "ogg", 'mkv'):
180         raise Http404
181     audiobook = get_object_or_404(models.Audiobook, id=aid)
182     field = which
183     if which == 'mkv':
184         field = 'youtube'
185     file_ = getattr(audiobook, "%s_file" % field)
186     if not file_:
187         raise Http404
188     ext = file_.path.rsplit('.', 1)[-1]
189     response = HttpResponse(content_type='application/force-download')
190     
191     response['Content-Disposition'] = "attachment; filename*=UTF-8''%s.%s" % (
192         quote(audiobook.title.encode('utf-8'), safe=''), ext)
193     with open(file_.path, 'rb') as f:
194         response.write(f.read())
195     #response['X-Sendfile'] = file_.path.encode('utf-8')
196     return response
197
198
199 def list_publishing(request):
200     objects = models.Audiobook.objects.exclude(
201         mp3_status=None, ogg_status=None, youtube_status=None
202     ).order_by("youtube_queued", "title")
203     objects_by_status = {}
204     for o in objects:
205         statuses = set()
206         if o.mp3_status:
207             statuses.add((o.mp3_status, o.get_mp3_status_display()))
208         if o.ogg_status:
209             statuses.add((o.ogg_status, o.get_ogg_status_display()))
210         if o.youtube_status:
211             statuses.add((o.youtube_status, o.get_youtube_status_display()))
212         for status in statuses:
213             objects_by_status.setdefault(status, []).append(o)
214     status_objects = sorted(objects_by_status.items(), reverse=True)
215
216     return render(request, "archive/list_publishing.html", locals())
217
218
219 class AudiobookList(ListView):
220     def get_queryset(self):
221         qs = models.Audiobook.objects.all()
222         if 's' in self.request.GET:
223             qs = qs.annotate(s=SearchVector('title', 'slug')).filter(s=self.request.GET['s'])
224         return qs
225
226
227 @permission_required('archive.change_audiobook')
228 def file_managed(request, id):
229     audiobook = get_object_or_404(models.Audiobook, id=id)
230
231     if request.POST:
232         form = AudiobookForm(request.POST, instance=audiobook)
233         if form.is_valid():
234             try:
235                 form.save()
236             except IOError:
237                 raise Http404
238
239     tags = {}
240     if audiobook.source_file:
241         path = audiobook.source_file.path[len(settings.FILES_PATH):].lstrip('/')
242
243         # for tags update
244         tags = mutagen.File(audiobook.source_file.path.encode('utf-8'))
245         if not tags:
246             tags = {}
247     form = AudiobookForm(instance=audiobook)
248
249     user_can_publish = (
250         request.user.is_authenticated and
251         request.user.oauthconnection_set.filter(access=True).exists())
252
253     alerts = []
254     parts_count = audiobook.parts_count
255     if parts_count > 1:
256         series = models.Audiobook.objects.filter(slug=audiobook.slug)
257         if not audiobook.index:
258             alerts.append(_('There is more than one part, but index is not set.'))
259         if set(series.values_list('index', flat=True)) != set(range(1, parts_count + 1)):
260             alerts.append(_('Part indexes are not 1..%(parts_count)d.') % {"parts_count": parts_count})
261
262     from youtube.models import YouTube
263     youtube = YouTube.objects.first()
264     youtube_title = youtube.get_title(audiobook)
265     youtube_description = youtube.get_description(audiobook)
266
267             
268     return render(request, "archive/file_managed.html", locals())
269
270
271 def list_unmanaged(request):
272     objects = sorted(all_files(settings.UNMANAGED_PATH))
273     return render(request, "archive/list_unmanaged.html", locals())
274
275
276 def file_unmanaged(request, filename):
277     tags = mutagen.File(os.path.join(settings.UNMANAGED_PATH, filename.encode('utf-8')))
278     if not tags:
279         tags = {}
280     
281     err_exists = request.GET.get('exists')
282     return render(request, "archive/file_unmanaged.html", locals())
283
284
285 class BookView(ListView):
286     template_name = 'archive/book.html'
287
288     def get_queryset(self):
289         qs = models.Audiobook.objects.filter(slug=self.kwargs["slug"]).order_by(
290             "index"
291         )
292         last_vol = None
293         last_vol_sub = None
294         for b in qs:
295             if last_vol is None or last_vol.youtube_volume_index != b.youtube_volume_index:
296                 last_vol = b
297                 b.total = 0
298                 if last_vol_sub is None or b.youtube_volume:
299                     last_vol_sub = last_vol
300                     last_vol_sub.total_for_sub = 0
301             last_vol.total += b.duration
302             last_vol_sub.total_for_sub += b.duration
303             b.subtotal = last_vol_sub.total_for_sub
304         return list(qs)
305
306
307 @permission_required('archive.change_audiobook')
308 def book_youtube_volume(request, aid):
309     audiobook = get_object_or_404(models.Audiobook, id=aid)
310     slug = audiobook.slug
311     cur_vol = audiobook.youtube_volume
312     new_vol = request.POST.get('volume', '')
313
314     audiobook.youtube_volume = new_vol
315     audiobook.save()
316     
317     for a in models.Audiobook.objects.filter(slug=slug, youtube_volume=cur_vol, index__gt=audiobook.index).order_by('index'):
318         if a.youtube_volume != cur_vol:
319             break
320         a.youtube_volume = new_vol
321         a.save()
322     
323     return redirect('book', audiobook.slug)
324