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