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