]> git.0d.be Git - django-panik-emissions.git/blob - emissions/management/commands/create-sound-files.py
add possibility to request higher quality for some podcasts
[django-panik-emissions.git] / emissions / management / commands / create-sound-files.py
1 from __future__ import print_function
2
3 import base64
4 import mutagen
5 import mutagen.mp3
6 import shutil
7 import os
8 import subprocess
9
10 from django.core.management.base import BaseCommand, CommandError
11
12 from ...models import SoundFile
13
14
15 def get_bitrate(filename):
16     p = subprocess.Popen(['mediainfo', '--Inform=Audio;%BitRate%', filename],
17                          close_fds=True,
18                          stdin=subprocess.PIPE,
19                          stdout=subprocess.PIPE,
20                          stderr=subprocess.PIPE)
21     stdout, stderr = p.communicate()
22     try:
23         return int(stdout) / 1000
24     except ValueError:
25         return None
26
27
28 class Command(BaseCommand):
29
30     def add_arguments(self, parser):
31         parser.add_argument(
32             '--force',
33             action='store_true',
34             dest='force',
35             default=False,
36             help='Create files even if they exist')
37         parser.add_argument('--reset-metadata',
38             action='store_true',
39             dest='reset_metadata',
40             default=False,
41             help='Reset metadata on all files')
42         parser.add_argument('--emission',
43             dest='emission',
44             metavar='EMISSION',
45             default=None,
46             help='Process files belonging to emission only')
47         parser.add_argument('--episode',
48             dest='episode',
49             metavar='EPISODE',
50             default=None,
51             help='Process files belonging to episode only')
52         parser.add_argument('--formats',
53             dest='formats',
54             default='ogg,mp3',
55             help='File formats')
56         parser.add_argument('--copy',
57             action='store_true',
58             dest='copy',
59             default=False,
60             help='Copy initial file')
61         parser.add_argument('--link',
62             action='store_true',
63             dest='link',
64             default=False,
65             help='Link initial file')
66
67     def handle(self, force, reset_metadata, copy, link, emission, episode, verbosity, formats, **kwargs):
68         self.verbose = (verbosity > 1)
69         self.copy = copy
70         self.link = link
71
72         for soundfile in SoundFile.objects.select_related():
73             if emission and soundfile.episode.emission.slug != emission:
74                 continue
75             if episode and soundfile.episode.slug != episode:
76                 continue
77             try:
78                 if soundfile.file is None or not os.path.exists(soundfile.file.path):
79                     continue
80             except ValueError:  # no file associated with it
81                 continue
82             if not soundfile.podcastable:
83                 # get duration using initial file
84                 if not soundfile.duration:
85                     cmd = ['soxi', '-D', soundfile.file.path]
86                     try:
87                         soundfile.duration = int(float(subprocess.check_output(cmd)))
88                     except (ValueError, subprocess.CalledProcessError):
89                         pass
90                     else:
91                         soundfile.save()
92                 continue
93             for format in formats.split(','):
94                 file_path = soundfile.get_format_path(format)
95                 created = False
96                 if not os.path.exists(file_path) or force:
97                     created = self.create(soundfile, format)
98                 if created or reset_metadata:
99                     self.set_metadata(soundfile, format)
100             if (force or not soundfile.duration):
101                 for extension in ('ogg', 'mp3'):
102                     soundfile_name = soundfile.get_format_path(extension)
103                     if os.path.exists(soundfile_name):
104                         cmd = ['soxi', '-D', soundfile_name]
105                         soundfile.duration = int(float(subprocess.check_output(cmd)))
106                         soundfile.save()
107                         break
108
109
110     def create(self, soundfile, format):
111         file_path = soundfile.get_format_path(format)
112         if not os.path.exists(os.path.dirname(file_path)):
113             os.makedirs(os.path.dirname(file_path))
114
115         if self.copy and os.path.splitext(soundfile.file.path)[-1].strip('.') == format:
116             shutil.copy(soundfile.file.path, file_path)
117             return
118
119         if self.link and os.path.splitext(soundfile.file.path)[-1].strip('.') == format:
120             os.symlink(soundfile.file.path, file_path)
121             return
122
123         vorbis_rates = [64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500]
124         orig_bitrate = get_bitrate(soundfile.file.path)
125         vorbis_q = 5
126         mp3_q = 4
127         if orig_bitrate is not None:
128             if soundfile.episode.emission.podcast_sound_quality == 'high':
129                 vorbis_q = 9
130                 mp3_q = 9
131             # cap quality to original bitrate (using vorbis quality mapping)
132             for i, rate in enumerate(vorbis_rates):
133                 if orig_bitrate < rate:
134                     vorbis_q = min((i, vorbis_q))
135                     mp3_q = min((i, mp3_q))
136                     break
137
138         cmd = ['ffmpeg', '-y', '-i', soundfile.file.path]
139         if format == 'ogg':
140             cmd.extend(['-q:a', str(vorbis_q)])
141         elif format == 'mp3':
142             cmd.extend(['-q:a', str(mp3_q)])
143
144         cmd.append(file_path)
145
146         if self.verbose:
147             print('creating', file_path)
148             print('  ', ' '.join(cmd))
149         else:
150             cmd[1:1] = ['-loglevel', 'quiet']
151
152         subprocess.call(cmd)
153         return os.path.exists(file_path)
154
155     def set_metadata(self, soundfile, format):
156         file_path = soundfile.get_format_path(format)
157
158         audio = mutagen.File(file_path, easy=True)
159
160         if 'comment' in audio:
161             del audio['comment']
162         if soundfile.fragment is True and soundfile.title:
163             audio['title'] = '%s - %s' % (
164                     soundfile.episode.title,
165                     soundfile.title)
166         else:
167             audio['title'] = soundfile.episode.title
168         audio['album'] = soundfile.episode.emission.title
169         audio['artist'] = 'Radio Panik'
170
171         if soundfile.episode.image or soundfile.episode.emission.image:
172             image = (soundfile.episode.image or soundfile.episode.emission.image)
173             image.file.open()
174             if os.path.splitext(image.path)[1].lower() in ('.jpeg', '.jpg'):
175                 mimetype = 'image/jpeg'
176             elif os.path.splitext(image.path)[1].lower() == '.png':
177                 mimetype = 'image/png'
178             else:
179                 mimetype = None
180             if mimetype:
181                 if format == 'ogg':
182                     audio['coverartmime'] = mimetype
183                     audio['coverartdescription'] = 'image'
184                     audio['coverart'] = base64.encodebytes(image.read()).replace(b'\n', b'').decode('ascii')
185                 elif format == 'mp3':
186                     audio.save()
187                     audio = mutagen.mp3.MP3(file_path)
188                     audio.tags.add(mutagen.id3.APIC(
189                             encoding=3, description='image',
190                             type=3, mime=mimetype, data=image.read()))
191             image.close()
192
193         audio.save()