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