use directories and allow filetypes other than FLAC
[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             '-acodec', 'libmp3lame',
166             out_path
167             ])
168
169     @classmethod
170     def set_tags(cls, audiobook, file_name):
171         audio = id3.ID3(file_name)
172         for k, v in audiobook.mp3_tags['tags'].items():
173             factory_tuple = cls.TAG_MAP[k]
174             factory, tagtype = factory_tuple[:2]
175             audio.add(factory(tagtype, v, *factory_tuple[2:]))
176
177         if COVER_IMAGE:
178             mime = mimetypes.guess_type(COVER_IMAGE)
179             f = open(COVER_IMAGE)
180             audio.add(id3.APIC(encoding=0, mime=mime, type=3, desc=u'', data=f.read()))
181             f.close()
182
183         audio.save()
184
185
186 class OggTask(AudioFormatTask):
187     ext = 'ogg'
188
189     @staticmethod
190     def encode(in_path, out_path):
191         # 44.1kHz 64kbps mono Ogg Vorbis
192         subprocess.check_call(['ffmpeg', 
193             '-i', in_path,
194             '-ar', '44100',
195             '-ab', '64k',
196             '-ac', '1',
197             '-y',
198             '-acodec', 'libvorbis',
199             out_path
200             ])