ae739307eaf3fd771994f761917f59b3445e0f39
[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, publish=True):
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         self.save(audiobook, out_file.name)
114
115         if publish:
116             self.put(audiobook, out_file.name)
117             self.published(aid)
118         else:
119             self.set_status(aid, None)
120
121     def on_failure(self, exc, task_id, args, kwargs, einfo):
122         aid = (args[0], kwargs.get('aid'))[0]
123         self.set_status(aid, None)
124
125
126 class Mp3Task(AudioFormatTask):
127     ext = 'mp3'
128
129     # these shouldn't be staticmethods
130     def id3_text(tag, text):
131         return tag(encoding=1, text=text)
132     def id3_url(tag, text):
133         return tag(url=text)
134     def id3_comment(tag, text, lang=u'pol'):
135         return tag(encoding=1, lang=lang, desc=u'', text=text)
136     def id3_priv(tag, text, what=u''):
137         return tag(owner='wolnelektury.pl?%s' % what, data=text.encode('utf-8'))
138
139     TAG_MAP = {
140         'album': (id3_text, id3.TALB),
141         'albumartist': (id3_text, id3.TPE2),
142         'artist': (id3_text, id3.TPE1),
143         'conductor': (id3_text, id3.TPE3),
144         'copyright': (id3_text, id3.TCOP),
145         'date': (id3_text, id3.TDRC),
146         'genre': (id3_text, id3.TCON),
147         'language': (id3_text, id3.TLAN),
148         'organization': (id3_text, id3.TPUB),
149         'title': (id3_text, id3.TIT2),
150         'comment': (id3_comment, id3.COMM, 'pol'),
151         'contact': (id3_url, id3.WOAF),
152         'license': (id3_url, id3.WCOP),
153         'flac_sha1': (id3_priv, id3.PRIV, 'flac_sha1'),
154         'project': (id3_priv, id3.PRIV, 'project'),
155         'funded_by': (id3_priv, id3.PRIV, 'funded_by'),
156     }
157
158     @staticmethod
159     def encode(in_path, out_path):
160         # 44.1kHz 64kbps mono MP3
161         subprocess.check_call(['ffmpeg', 
162             '-i', in_path.encode('utf-8'),
163             '-ar', '44100',
164             '-ab', '64k',
165             '-ac', '1',
166             '-y',
167             '-acodec', 'libmp3lame',
168             out_path.encode('utf-8')
169             ])
170
171     @classmethod
172     def set_tags(cls, audiobook, file_name):
173         audio = id3.ID3(file_name)
174         for k, v in audiobook.mp3_tags['tags'].items():
175             factory_tuple = cls.TAG_MAP[k]
176             factory, tagtype = factory_tuple[:2]
177             audio.add(factory(tagtype, v, *factory_tuple[2:]))
178
179         if COVER_IMAGE:
180             mime = mimetypes.guess_type(COVER_IMAGE)
181             f = open(COVER_IMAGE)
182             audio.add(id3.APIC(encoding=0, mime=mime, type=3, desc=u'', data=f.read()))
183             f.close()
184
185         audio.save()
186
187
188 class OggTask(AudioFormatTask):
189     ext = 'ogg'
190
191     @staticmethod
192     def encode(in_path, out_path):
193         # 44.1kHz 64kbps mono Ogg Vorbis
194         subprocess.check_call(['ffmpeg', 
195             '-i', in_path.encode('utf-8'),
196             '-ar', '44100',
197             '-ab', '64k',
198             '-ac', '1',
199             '-y',
200             '-acodec', 'libvorbis',
201             out_path.encode('utf-8')
202             ])