1# -*- coding: utf-8 -*-
2# Convertes m4a audio files to mp3
3# This requires ffmpeg to be installed. Also works as a context
4# menu item for already-downloaded files.
5#
6# (c) 2011-11-23 Bernd Schlapsi <brot@gmx.info>
7# Released under the same license terms as gPodder itself.
8
9import logging
10import os
11import subprocess
12
13import gpodder
14from gpodder import util
15
16logger = logging.getLogger(__name__)
17
18_ = gpodder.gettext
19
20__title__ = _('Convert audio files')
21__description__ = _('Transcode audio files to mp3/ogg')
22__authors__ = 'Bernd Schlapsi <brot@gmx.info>, Thomas Perl <thp@gpodder.org>'
23__doc__ = 'https://gpodder.github.io/docs/extensions/audioconverter.html'
24__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/AudioConverter'
25__category__ = 'post-download'
26
27
28DefaultConfig = {
29    'use_opus': False,  # Set to True to convert to .opus
30    'use_ogg': False,  # Set to True to convert to .ogg
31    'context_menu': True,  # Show the conversion option in the context menu
32}
33
34
35class gPodderExtension:
36    MIME_TYPES = ('audio/x-m4a', 'audio/mp4', 'audio/mp4a-latm', 'audio/mpeg', 'audio/ogg', 'audio/opus')
37    EXT = ('.m4a', '.ogg', '.opus', '.mp3')
38    CMD = {'avconv': {'.mp3': ['-n', '-i', '%(old_file)s', '-q:a', '2', '-id3v2_version', '3', '-write_id3v1', '1', '%(new_file)s'],
39                      '.ogg': ['-n', '-i', '%(old_file)s', '-q:a', '2', '%(new_file)s'],
40                      '.opus': ['-n', '-i', '%(old_file)s', '-b:a', '64k', '%(new_file)s']
41                      },
42           'ffmpeg': {'.mp3': ['-n', '-i', '%(old_file)s', '-q:a', '2', '-id3v2_version', '3', '-write_id3v1', '1', '%(new_file)s'],
43                      '.ogg': ['-n', '-i', '%(old_file)s', '-q:a', '2', '%(new_file)s'],
44                      '.opus': ['-n', '-i', '%(old_file)s', '-b:a', '64k', '%(new_file)s']
45                      }
46           }
47
48    def __init__(self, container):
49        self.container = container
50        self.config = self.container.config
51
52        # Dependency checks
53        self.command = self.container.require_any_command(['avconv', 'ffmpeg'])
54
55        # extract command without extension (.exe on Windows) from command-string
56        self.command_without_ext = os.path.basename(os.path.splitext(self.command)[0])
57
58    def on_episode_downloaded(self, episode):
59        self._convert_episode(episode)
60
61    def _get_new_extension(self):
62        if self.config.use_ogg:
63            extension = '.ogg'
64        elif self.config.use_opus:
65            extension = '.opus'
66        else:
67            extension = '.mp3'
68        return extension
69
70    def _check_source(self, episode):
71        if episode.extension() == self._get_new_extension():
72            return False
73
74        if episode.mime_type in self.MIME_TYPES:
75            return True
76
77        # Also check file extension (bug 1770)
78        if episode.extension() in self.EXT:
79            return True
80
81        return False
82
83    def on_episodes_context_menu(self, episodes):
84        if not self.config.context_menu:
85            return None
86
87        if not all(e.was_downloaded(and_exists=True) for e in episodes):
88            return None
89
90        if not any(self._check_source(episode) for episode in episodes):
91            return None
92
93        menu_item = _('Convert to %(format)s') % {'format': self._target_format()}
94
95        return [(menu_item, self._convert_episodes)]
96
97    def _target_format(self):
98        if self.config.use_ogg:
99            target_format = 'OGG'
100        elif self.config.use_opus:
101            target_format = 'OPUS'
102        else:
103            target_format = 'MP3'
104        return target_format
105
106    def _convert_episode(self, episode):
107        if not self._check_source(episode):
108            return
109
110        new_extension = self._get_new_extension()
111        old_filename = episode.local_filename(create=False)
112        filename, old_extension = os.path.splitext(old_filename)
113        new_filename = filename + new_extension
114
115        cmd_param = self.CMD[self.command_without_ext][new_extension]
116        cmd = [self.command] + \
117            [param % {'old_file': old_filename, 'new_file': new_filename}
118                for param in cmd_param]
119
120        if gpodder.ui.win32:
121            ffmpeg = util.Popen(cmd)
122            ffmpeg.wait()
123            stdout, stderr = ("<unavailable>",) * 2
124        else:
125            ffmpeg = util.Popen(cmd, stdout=subprocess.PIPE,
126                    stderr=subprocess.PIPE)
127            stdout, stderr = ffmpeg.communicate()
128
129        if ffmpeg.returncode == 0:
130            util.rename_episode_file(episode, new_filename)
131            os.remove(old_filename)
132
133            logger.info('Converted audio file to %(format)s.' % {'format': new_extension})
134            gpodder.user_extensions.on_notification_show(_('File converted'), episode.title)
135        else:
136            logger.warn('Error converting audio file: %s / %s', stdout, stderr)
137            gpodder.user_extensions.on_notification_show(_('Conversion failed'), episode.title)
138
139    def _convert_episodes(self, episodes):
140        # not running in background because there is no feedback to the user
141        # which one is being converted and nothing prevents from clicking convert twice.
142        for episode in episodes:
143            self._convert_episode(episode)
144