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