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