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