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