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