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