Use FILE_UPLOAD_TEMP_DIR for big files.
[audio.git] / src / archive / tasks.py
1 from datetime import datetime
2 import errno
3 import mimetypes
4 import os
5 import os.path
6 import pipes
7 import stat
8 import subprocess
9 from tempfile import NamedTemporaryFile
10 from time import sleep
11
12 from celery.task import Task
13 from django.conf import settings
14 from django.db.models import F
15 from django.contrib.auth.models import User
16 from mutagen import File
17 from mutagen import id3
18
19 from apiclient import api_call
20 from archive.constants import status
21 from archive.models import Audiobook
22 from archive.settings import COVER_IMAGE, UPLOAD_URL
23 from archive.utils import ExistingFile
24
25
26 class AudioFormatTask(Task):
27     abstract = True
28
29     class RemoteOperationError(BaseException):
30         pass
31
32     @classmethod
33     def set_status(cls, aid, status):
34         Audiobook.objects.filter(pk=aid).update(
35             **{'%s_status' % cls.prefix: status})
36
37     @staticmethod
38     def encode(in_path, out_path):
39         raise NotImplemented
40
41     @classmethod
42     def set_tags(cls, audiobook, file_name):
43         tags = getattr(audiobook, "get_%s_tags" % cls.prefix)()['tags']
44         if not tags.get('flac_sha1'):
45             tags['flac_sha1'] = audiobook.get_source_sha1()
46         audio = File(file_name)
47         for k, v in tags.items():
48             audio[k] = v
49         audio.save()
50
51     @classmethod
52     def save(cls, audiobook, file_name):
53         field = "%s_file" % cls.prefix
54         getattr(audiobook, field).save(
55             "%d.%s" % (audiobook.pk, cls.ext),
56             ExistingFile(file_name),
57             save=False
58             )
59         os.chmod(getattr(audiobook, field).path, stat.S_IREAD|stat.S_IWRITE|stat.S_IRGRP|stat.S_IROTH)
60         Audiobook.objects.filter(pk=audiobook.pk).update(
61             **{field: getattr(audiobook, field)})
62
63     @classmethod
64     def published(cls, aid):
65         kwargs = {
66             "%s_published_tags" % cls.prefix: F("%s_tags" % cls.prefix),
67             "%s_tags" % cls.prefix: None,
68             "%s_published" % cls.prefix: datetime.now(),
69             '%s_status' % cls.prefix: None,
70         }
71         Audiobook.objects.filter(pk=aid).update(**kwargs)
72
73     @classmethod
74     def put(cls, user, audiobook, path):
75         tags = getattr(audiobook, "get_%s_tags" % cls.prefix)()
76         data = {
77             'book': tags['url'],
78             'type': cls.ext,
79             'name': tags['name'],
80             'part_name': audiobook.part_name,
81             'part_index': audiobook.index,
82             'parts_count': audiobook.parts_count,
83             'source_sha1': audiobook.source_sha1,
84
85             'project_description': audiobook.project.get_description(),
86             'project_icon': audiobook.project.get_icon_url(),
87         }
88         with open(path, 'rb') as f:
89             api_call(user, UPLOAD_URL, data=data, files={
90                 "file": f,
91             })
92
93     def run(self, uid, aid, publish=True):
94         aid = int(aid)
95         audiobook = Audiobook.objects.get(id=aid)
96         self.audiobook = audiobook
97         self.set_status(aid, status.ENCODING)
98
99         if uid:
100             user = User.objects.get(id=uid)
101         else:
102             user = None
103
104         out_file = NamedTemporaryFile(
105             delete=False, prefix='%d-' % aid, suffix='.%s' % self.ext,
106             dir=settings.FILE_UPLOAD_TEMP_DIR
107         )
108         out_file.close()
109         self.encode(self.get_source_file_paths(audiobook), out_file.name)
110         self.set_status(aid, status.TAGGING)
111         self.set_tags(audiobook, out_file.name)
112         self.set_status(aid, status.SENDING)
113
114         if publish:
115             self.put(user, audiobook, out_file.name)
116             self.published(aid)
117         else:
118             self.set_status(aid, None)
119
120         self.save(audiobook, out_file.name)
121
122     def get_source_file_paths(self, audiobook):
123         return [audiobook.source_file.path]
124
125     def on_failure(self, exc, task_id, args, kwargs, einfo):
126         aid = (args[0], kwargs.get('aid'))[0]
127         self.set_status(aid, None)
128
129
130 class Mp3Task(AudioFormatTask):
131     prefix = ext = 'mp3'
132
133     # these shouldn't be staticmethods
134     def id3_text(tag, text):
135         return tag(encoding=3, text=text)
136     def id3_url(tag, text):
137         return tag(url=text)
138     def id3_comment(tag, text, lang='pol'):
139         return tag(encoding=3, lang=lang, desc='', text=text)
140     def id3_priv(tag, text, what=''):
141         return tag(owner='wolnelektury.pl?%s' % what, data=text.encode('utf-8'))
142
143     TAG_MAP = {
144         'album': (id3_text, id3.TALB),
145         'albumartist': (id3_text, id3.TPE2),
146         'artist': (id3_text, id3.TPE1),
147         'conductor': (id3_text, id3.TPE3),
148         'copyright': (id3_text, id3.TCOP),
149         'date': (id3_text, id3.TDRC),
150         'genre': (id3_text, id3.TCON),
151         'language': (id3_text, id3.TLAN),
152         'organization': (id3_text, id3.TPUB),
153         'title': (id3_text, id3.TIT2),
154         'comment': (id3_comment, id3.COMM, 'pol'),
155         'contact': (id3_url, id3.WOAF),
156         'license': (id3_url, id3.WCOP),
157         'flac_sha1': (id3_priv, id3.PRIV, 'flac_sha1'),
158         'project': (id3_priv, id3.PRIV, 'project'),
159         'funded_by': (id3_priv, id3.PRIV, 'funded_by'),
160     }
161
162     @staticmethod
163     def encode(in_paths, out_path):
164         assert len(in_paths) == 1
165         in_path = in_paths[0]
166         # 44.1kHz 64kbps mono MP3
167         subprocess.check_call(['ffmpeg', 
168             '-i', in_path.encode('utf-8'),
169             '-ar', '44100',
170             '-ab', '64k',
171             '-ac', '1',
172             '-y',
173             '-acodec', 'libmp3lame',
174             out_path.encode('utf-8')
175             ])
176
177     @classmethod
178     def set_tags(cls, audiobook, file_name):
179         mp3_tags = audiobook.get_mp3_tags()['tags']
180         if not mp3_tags.get('flac_sha1'):
181             mp3_tags['flac_sha1'] = audiobook.get_source_sha1()
182         audio = id3.ID3(file_name)
183         for k, v in mp3_tags.items():
184             factory_tuple = cls.TAG_MAP[k]
185             factory, tagtype = factory_tuple[:2]
186             audio.add(factory(tagtype, v, *factory_tuple[2:]))
187
188         if COVER_IMAGE:
189             mime = mimetypes.guess_type(COVER_IMAGE)
190             with open(COVER_IMAGE, 'rb') as f:
191                 audio.add(id3.APIC(encoding=0, mime=mime, type=3, desc='', data=f.read()))
192
193         audio.save()
194
195
196 class OggTask(AudioFormatTask):
197     prefix = ext = 'ogg'
198
199     @staticmethod
200     def encode(in_paths, out_path):
201         assert len(in_paths) == 1
202         in_path = in_paths[0]
203         # 44.1kHz 64kbps mono Ogg Vorbis
204         subprocess.check_call(['ffmpeg', 
205             '-i', in_path.encode('utf-8'),
206             '-ar', '44100',
207             '-ab', '64k',
208             '-ac', '1',
209             '-y',
210             '-acodec', 'libvorbis',
211             out_path.encode('utf-8')
212             ])