More automation for YouTube: volume management (WiP).
[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.save()
190     return redirect(file_managed, aid)
191
192
193 def download(request, aid, which="source"):
194     if which not in ("source", "mp3", "ogg", 'mkv'):
195         raise Http404
196     audiobook = get_object_or_404(models.Audiobook, id=aid)
197     field = which
198     if which == 'mkv':
199         field = 'youtube'
200     file_ = getattr(audiobook, "%s_file" % field)
201     if not file_:
202         raise Http404
203     ext = file_.path.rsplit('.', 1)[-1]
204     response = HttpResponse(content_type='application/force-download')
205     
206     response['Content-Disposition'] = "attachment; filename*=UTF-8''%s.%s" % (
207         quote(audiobook.title.encode('utf-8'), safe=''), ext)
208     with open(file_.path, 'rb') as f:
209         response.write(f.read())
210     #response['X-Sendfile'] = file_.path.encode('utf-8')
211     return response
212
213
214 def list_unpublished(request):
215     division = 'unpublished'
216
217     objects = models.Audiobook.objects.filter(Q(mp3_published=None) | Q(ogg_published=None))
218     return render(request, "archive/list_unpublished.html", locals())
219
220
221 def list_publishing(request):
222     division = 'publishing'
223
224     objects = models.Audiobook.objects.exclude(mp3_status=None, ogg_status=None, youtube_status=None)
225     objects_by_status = {}
226     for o in objects:
227         statuses = set()
228         if o.mp3_status:
229             statuses.add((o.mp3_status, o.get_mp3_status_display()))
230         if o.ogg_status:
231             statuses.add((o.ogg_status, o.get_ogg_status_display()))
232         if o.youtube_status:
233             statuses.add((o.youtube_status, o.get_youtube_status_display()))
234         for status in statuses:
235             objects_by_status.setdefault(status, []).append(o)
236     status_objects = sorted(objects_by_status.items(), reverse=True)
237
238     return render(request, "archive/list_publishing.html", locals())
239
240
241 def list_published(request):
242     division = 'published'
243
244     objects = models.Audiobook.objects.exclude(Q(mp3_published=None) | Q(ogg_published=None))
245     return render(request, "archive/list_published.html", locals())
246
247
248 @permission_required('archive.change_audiobook')
249 def file_managed(request, id):
250     audiobook = get_object_or_404(models.Audiobook, id=id)
251
252     if request.POST:
253         form = AudiobookForm(request.POST, instance=audiobook)
254         if form.is_valid():
255             try:
256                 form.save()
257             except IOError:
258                 raise Http404
259
260     division = 'published' if audiobook.published() else 'unpublished'
261     path = audiobook.source_file.path[len(settings.FILES_PATH):].lstrip('/')
262
263     # for tags update
264     tags = mutagen.File(audiobook.source_file.path.encode('utf-8'))
265     if not tags:
266         tags = {}
267     form = AudiobookForm(instance=audiobook)
268
269     user_can_publish = (
270         request.user.is_authenticated and
271         request.user.oauthconnection_set.filter(access=True).exists())
272
273     alerts = []
274     parts_count = audiobook.parts_count
275     if parts_count > 1:
276         series = models.Audiobook.objects.filter(slug=audiobook.slug)
277         if not audiobook.index:
278             alerts.append(_('There is more than one part, but index is not set.'))
279         if set(series.values_list('index', flat=True)) != set(range(1, parts_count + 1)):
280             alerts.append(_('Part indexes are not 1..%(parts_count)d.') % {"parts_count": parts_count})
281
282     return render(request, "archive/file_managed.html", locals())
283
284
285 def list_unmanaged(request):
286     division = 'unmanaged'
287
288     objects = sorted(all_files(settings.UNMANAGED_PATH))
289     return render(request, "archive/list_unmanaged.html", locals())
290
291
292 def file_unmanaged(request, filename):
293     division = 'unmanaged'
294
295     tags = mutagen.File(os.path.join(settings.UNMANAGED_PATH, filename.encode('utf-8')))
296     if not tags:
297         tags = {}
298     
299     err_exists = request.GET.get('exists')
300     return render(request, "archive/file_unmanaged.html", locals())
301
302
303 class BookView(ListView):
304     template_name = 'archive/book.html'
305
306     def get_queryset(self):
307         return models.Audiobook.objects.filter(slug=self.kwargs['slug'])