diff options
-rw-r--r-- | .tarignore | 17 | ||||
-rw-r--r-- | .travis.yml | 8 | ||||
-rw-r--r-- | LATEST_VERSION | 2 | ||||
-rw-r--r-- | Makefile | 23 | ||||
-rw-r--r-- | README.md | 10 | ||||
-rwxr-xr-x | devscripts/release.sh | 60 | ||||
-rw-r--r-- | test/tests.json | 34 | ||||
-rwxr-xr-x | youtube-dl | bin | 3447 -> 55751 bytes | |||
-rw-r--r-- | youtube_dl/FileDownloader.py | 23 | ||||
-rwxr-xr-x | youtube_dl/InfoExtractors.py | 66 | ||||
-rw-r--r-- | youtube_dl/PostProcessor.py | 114 | ||||
-rw-r--r-- | youtube_dl/__init__.py | 23 | ||||
-rw-r--r-- | youtube_dl/utils.py | 3 | ||||
-rw-r--r-- | youtube_dl/version.py | 2 |
14 files changed, 227 insertions, 158 deletions
diff --git a/.tarignore b/.tarignore deleted file mode 100644 index 986afeed6..000000000 --- a/.tarignore +++ /dev/null @@ -1,17 +0,0 @@ -updates_key.pem -*.pyc -*.pyo -youtube-dl.exe -wine-py2exe/ -py2exe.log -*.kate-swp -build/ -dist/ -MANIFEST -*.DS_Store -youtube-dl.tar.gz -.coverage -cover/ -__pycache__/ -.git/ -*~ diff --git a/.travis.yml b/.travis.yml index 31eea852c..0687c8957 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ notifications: email: - filippo.valsorda@gmail.com - phihag@phihag.de - irc: - channels: - - "irc.freenode.org#youtube-dl" - skip_join: true +# irc: +# channels: +# - "irc.freenode.org#youtube-dl" +# skip_join: true diff --git a/LATEST_VERSION b/LATEST_VERSION index 275de03d9..a334573b6 100644 --- a/LATEST_VERSION +++ b/LATEST_VERSION @@ -1 +1 @@ -9999.99.99
\ No newline at end of file +2012.12.99 @@ -1,7 +1,7 @@ all: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion clean: - rm -rf youtube-dl youtube-dl.exe youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ + rm -rf youtube-dl youtube-dl.exe youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz PREFIX=/usr/local BINDIR=$(PREFIX)/bin @@ -20,7 +20,9 @@ test: #nosetests --with-coverage --cover-package=youtube_dl --cover-html --verbose --processes 4 test nosetests --verbose test -.PHONY: all clean install test +tar: youtube-dl.tar.gz + +.PHONY: all clean install test tar youtube-dl: youtube_dl/*.py zip --quiet youtube-dl youtube_dl/*.py @@ -42,6 +44,17 @@ youtube-dl.1: README.md youtube-dl.bash-completion: youtube_dl/*.py devscripts/bash-completion.in python devscripts/bash-completion.py -youtube-dl.tar.gz: all - tar -cvzf youtube-dl.tar.gz -s "|^./|./youtube-dl/|" \ - --exclude-from=".tarignore" -- . +youtube-dl.tar.gz: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion + @tar -czf youtube-dl.tar.gz --transform "s|^|youtube-dl/|" --owner 0 --group 0 \ + --exclude '*.DS_Store' \ + --exclude '*.kate-swp' \ + --exclude '*.pyc' \ + --exclude '*.pyo' \ + --exclude '*~' \ + --exclude '__pycache' \ + --exclude '.git' \ + -- \ + bin devscripts test youtube_dl \ + CHANGELOG LICENSE README.md README.txt \ + Makefile MANIFEST.in youtube-dl.1 youtube-dl.bash-completion setup.py \ + youtube-dl @@ -9,8 +9,8 @@ youtube-dl # DESCRIPTION **youtube-dl** is a small command-line program to download videos from YouTube.com and a few more sites. It requires the Python interpreter, version -2.x (x being at least 6), and it is not platform specific. It should work in -your Unix box, in Windows or in Mac OS X. It is released to the public domain, +2.6, 2.7, or 3.3+, and it is not platform specific. It should work on +your Unix box, on Windows or on Mac OS X. It is released to the public domain, which means you can modify it, redistribute it or use it however you like. # OPTIONS @@ -105,8 +105,8 @@ which means you can modify it, redistribute it or use it however you like. ## Post-processing Options: -x, --extract-audio convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe) - --audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", or "wav"; - best by default + --audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", "opus", or + "wav"; best by default --audio-quality QUALITY ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5) @@ -117,7 +117,7 @@ which means you can modify it, redistribute it or use it however you like. # CONFIGURATION -You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.local/config/youtube-dl.conf`. +You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.config/youtube-dl.conf`. # OUTPUT TEMPLATE diff --git a/devscripts/release.sh b/devscripts/release.sh index cf5784e8c..94f229f0e 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # IMPORTANT: the following assumptions are made # * the GH repo is on the origin remote @@ -47,39 +47,39 @@ REV=$(git rev-parse HEAD) make youtube-dl youtube-dl.tar.gz wget "http://jeromelaheurte.net:8142/download/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe || \ wget "http://jeromelaheurte.net:8142/build/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe -mkdir -p "update_staging/$version" -mv youtube-dl youtube-dl.exe "update_staging/$version" -mv youtube-dl.tar.gz "update_staging/$version/youtube-dl-$version.tar.gz" -RELEASE_FILES=youtube-dl youtube-dl.exe youtube-dl-$version.tar.gz -(cd update_staging/$version/ && md5sum $RELEASE_FILES > MD5SUMS) -(cd update_staging/$version/ && sha1sum $RELEASE_FILES > SHA1SUMS) -(cd update_staging/$version/ && sha256sum $RELEASE_FILES > SHA2-256SUMS) -(cd update_staging/$version/ && sha512sum $RELEASE_FILES > SHA2-512SUMS) +mkdir -p "build/$version" +mv youtube-dl youtube-dl.exe "build/$version" +mv youtube-dl.tar.gz "build/$version/youtube-dl-$version.tar.gz" +RELEASE_FILES="youtube-dl youtube-dl.exe youtube-dl-$version.tar.gz" +(cd build/$version/ && md5sum $RELEASE_FILES > MD5SUMS) +(cd build/$version/ && sha1sum $RELEASE_FILES > SHA1SUMS) +(cd build/$version/ && sha256sum $RELEASE_FILES > SHA2-256SUMS) +(cd build/$version/ && sha512sum $RELEASE_FILES > SHA2-512SUMS) git checkout HEAD -- youtube-dl youtube-dl.exe echo "\n### Signing and uploading the new binaries to youtube-dl.org..." -for f in $RELEASE_FILES; do gpg --detach-sig "update_staging/$version/$f"; done -scp -r "update_staging/$version" ytdl@youtube-dl.org:html/downloads/ -rm -r update_staging +for f in $RELEASE_FILES; do gpg --detach-sig "build/$version/$f"; done +scp -r "build/$version" ytdl@youtube-dl.org:html/downloads/ echo "\n### Now switching to gh-pages..." -git checkout gh-pages -git checkout "$MASTER" -- devscripts/gh-pages/ -git reset devscripts/gh-pages/ -devscripts/gh-pages/add-version.py $version -devscripts/gh-pages/sign-versions.py < updates_key.pem -devscripts/gh-pages/generate-download.py -devscripts/gh-pages/update-copyright.py -rm -r test_coverage -mv cover test_coverage -git add *.html *.html.in update test_coverage -git commit -m "release $version" -git show HEAD -read -p "Is it good, can I push? (y/n) " -n 1 -if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi -echo -git push origin gh-pages +git clone --branch gh-pages --single-branch . build/gh-pages +ROOT=$(pwd) +( + set -e + cd build/gh-pages + ORIGIN_URL=$(git config --get remote.origin.url) + "$ROOT/devscripts/gh-pages/add-version.py" $version + "$ROOT/devscripts/gh-pages/sign-versions.py" < updates_key.pem + "$ROOT/devscripts/gh-pages/generate-download.py" + "$ROOT/devscripts/gh-pages/update-copyright.py" + git add *.html *.html.in update + git commit -m "release $version" + git show HEAD + read -p "Is it good, can I push? (y/n) " -n 1 + if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi + echo + git push $ORIGIN_URL gh-pages +) +rm -r build echo "\n### DONE!" -rm -r devscripts -git checkout $MASTER diff --git a/test/tests.json b/test/tests.json index f4d7b2b69..540c5d183 100644 --- a/test/tests.json +++ b/test/tests.json @@ -178,5 +178,39 @@ "params": { "skip_download": true } + }, + { + "name": "ComedyCentral", + "url": "http://www.thedailyshow.com/full-episodes/thu-december-13-2012-kristen-stewart", + "playlist": [ + { + "file": "422204.mp4", + "md5": "7a7abe068b31ff03e7b8a37596e72380", + "info_dict": { + "title": "thedailyshow-thu-december-13-2012-kristen-stewart part 1" + } + }, + { + "file": "422205.mp4", + "md5": "30552b7274c94dbb933f64600eadddd2", + "info_dict": { + "title": "thedailyshow-thu-december-13-2012-kristen-stewart part 2" + } + }, + { + "file": "422206.mp4", + "md5": "1f4c0664b352cb8e8fe85d5da4fbee91", + "info_dict": { + "title": "thedailyshow-thu-december-13-2012-kristen-stewart part 3" + } + }, + { + "file": "422207.mp4", + "md5": "f61ee8a4e6bd1308438e03badad78554", + "info_dict": { + "title": "thedailyshow-thu-december-13-2012-kristen-stewart part 4" + } + } + ] } ] diff --git a/youtube-dl b/youtube-dl Binary files differindex e6f05c173..d87f13186 100755 --- a/youtube-dl +++ b/youtube-dl diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index be9e4918e..51df4c175 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -81,6 +81,7 @@ class FileDownloader(object): writesubtitles: Write the video subtitles to a .srt file subtitleslang: Language of the subtitles to download test: Download only first bytes to test the downloader. + keepvideo: Keep the video file after post-processing """ params = None @@ -529,13 +530,27 @@ class FileDownloader(object): return self._download_retcode def post_process(self, filename, ie_info): - """Run the postprocessing chain on the given file.""" + """Run all the postprocessors on the given file.""" info = dict(ie_info) info['filepath'] = filename + keep_video = None for pp in self._pps: - info = pp.run(info) - if info is None: - break + try: + keep_video_wish,new_info = pp.run(info) + if keep_video_wish is not None: + if keep_video_wish: + keep_video = keep_video_wish + elif keep_video is None: + # No clear decision yet, let IE decide + keep_video = keep_video_wish + except PostProcessingError as e: + self.to_stderr(u'ERROR: ' + e.msg) + if keep_video is False and not self.params.get('keepvideo', False): + try: + self.to_stderr(u'Deleting original file %s (pass -k to keep)' % filename) + os.remove(encodeFilename(filename)) + except (IOError, OSError): + self.to_stderr(u'WARNING: Unable to remove downloaded video file') def _download_with_rtmpdump(self, filename, url, player_url, page_url): self.report_destination(filename) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 83be8313f..092bfef22 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2333,7 +2333,6 @@ class ComedyCentralIE(InfoExtractor): (the-colbert-report-(videos|collections)/(?P<clipID>[0-9]+)/[^/]*/(?P<cntitle>.*?)) |(watch/(?P<date>[^/]*)/(?P<tdstitle>.*))))) $""" - IE_NAME = u'comedycentral' _available_formats = ['3500', '2200', '1700', '1200', '750', '400'] @@ -2361,16 +2360,12 @@ class ComedyCentralIE(InfoExtractor): def report_extraction(self, episode_id): self._downloader.to_screen(u'[comedycentral] %s: Extracting information' % episode_id) - def report_config_download(self, episode_id): - self._downloader.to_screen(u'[comedycentral] %s: Downloading configuration' % episode_id) + def report_config_download(self, episode_id, media_id): + self._downloader.to_screen(u'[comedycentral] %s: Downloading configuration for %s' % (episode_id, media_id)) def report_index_download(self, episode_id): self._downloader.to_screen(u'[comedycentral] %s: Downloading show index' % episode_id) - def report_player_url(self, episode_id): - self._downloader.to_screen(u'[comedycentral] %s: Determining player URL' % episode_id) - - def _print_formats(self, formats): print('Available formats:') for x in formats: @@ -2409,6 +2404,7 @@ class ComedyCentralIE(InfoExtractor): try: htmlHandle = compat_urllib_request.urlopen(req) html = htmlHandle.read() + webpage = html.decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: self._downloader.trouble(u'ERROR: unable to download webpage: %s' % compat_str(err)) return @@ -2423,29 +2419,20 @@ class ComedyCentralIE(InfoExtractor): return epTitle = mobj.group('episode') - mMovieParams = re.findall('(?:<param name="movie" value="|var url = ")(http://media.mtvnservices.com/([^"]*(?:episode|video).*?:.*?))"', html) + mMovieParams = re.findall('(?:<param name="movie" value="|var url = ")(http://media.mtvnservices.com/([^"]*(?:episode|video).*?:.*?))"', webpage) if len(mMovieParams) == 0: # The Colbert Report embeds the information in a without # a URL prefix; so extract the alternate reference # and then add the URL prefix manually. - altMovieParams = re.findall('data-mgid="([^"]*(?:episode|video).*?:.*?)"', html) + altMovieParams = re.findall('data-mgid="([^"]*(?:episode|video).*?:.*?)"', webpage) if len(altMovieParams) == 0: self._downloader.trouble(u'ERROR: unable to find Flash URL in webpage ' + url) return else: mMovieParams = [("http://media.mtvnservices.com/" + altMovieParams[0], altMovieParams[0])] - playerUrl_raw = mMovieParams[0][0] - self.report_player_url(epTitle) - try: - urlHandle = compat_urllib_request.urlopen(playerUrl_raw) - playerUrl = urlHandle.geturl() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to find out player URL: ' + compat_str(err)) - return - uri = mMovieParams[0][1] indexUrl = 'http://shadow.comedycentral.com/feeds/video_player/mrss/?' + compat_urllib_parse.urlencode({'uri': uri}) self.report_index_download(epTitle) @@ -2459,7 +2446,7 @@ class ComedyCentralIE(InfoExtractor): idoc = xml.etree.ElementTree.fromstring(indexXml) itemEls = idoc.findall('.//item') - for itemEl in itemEls: + for partNum,itemEl in enumerate(itemEls): mediaId = itemEl.findall('./guid')[0].text shortMediaId = mediaId.split(':')[-1] showId = mediaId.split(':')[-2].replace('.com', '') @@ -2469,7 +2456,7 @@ class ComedyCentralIE(InfoExtractor): configUrl = ('http://www.comedycentral.com/global/feeds/entertainment/media/mediaGenEntertainment.jhtml?' + compat_urllib_parse.urlencode({'uri': mediaId})) configReq = compat_urllib_request.Request(configUrl) - self.report_config_download(epTitle) + self.report_config_download(epTitle, shortMediaId) try: configXml = compat_urllib_request.urlopen(configReq).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: @@ -2491,7 +2478,7 @@ class ComedyCentralIE(InfoExtractor): return # For now, just pick the highest bitrate - format,video_url = turls[-1] + format,rtmp_video_url = turls[-1] # Get the format arg from the arg stream req_format = self._downloader.params.get('format', None) @@ -2499,18 +2486,16 @@ class ComedyCentralIE(InfoExtractor): # Select format if we can find one for f,v in turls: if f == req_format: - format, video_url = f, v + format, rtmp_video_url = f, v break - # Patch to download from alternative CDN, which does not - # break on current RTMPDump builds - broken_cdn = "rtmpe://viacomccstrmfs.fplive.net/viacomccstrm/gsp.comedystor/" - better_cdn = "rtmpe://cp10740.edgefcs.net/ondemand/mtvnorigin/gsp.comedystor/" - - if video_url.startswith(broken_cdn): - video_url = video_url.replace(broken_cdn, better_cdn) + m = re.match(r'^rtmpe?://.*?/(?P<finalid>gsp.comedystor/.*)$', rtmp_video_url) + if not m: + raise ExtractorError(u'Cannot transform RTMP url') + base = 'http://mtvnmobile.vo.llnwd.net/kip0/_pxn=1+_pxI0=Ripod-h264+_pxL0=undefined+_pxM0=+_pxK=18639+_pxE=mp4/44620/mtvnorigin/' + video_url = base + m.group('finalid') - effTitle = showId + u'-' + epTitle + effTitle = showId + u'-' + epTitle + u' part ' + compat_str(partNum+1) info = { 'id': shortMediaId, 'url': video_url, @@ -2521,9 +2506,7 @@ class ComedyCentralIE(InfoExtractor): 'format': format, 'thumbnail': None, 'description': officialTitle, - 'player_url': None #playerUrl } - results.append(info) return results @@ -2603,7 +2586,6 @@ class EscapistIE(InfoExtractor): return [info] - class CollegeHumorIE(InfoExtractor): """Information extractor for collegehumor.com""" @@ -3542,17 +3524,23 @@ class JustinTVIE(InfoExtractor): return response = json.loads(webpage) + if type(response) != list: + error_text = response.get('error', 'unknown error') + self._downloader.trouble(u'ERROR: Justin.tv API: %s' % error_text) + return info = [] for clip in response: video_url = clip['video_file_url'] if video_url: video_extension = os.path.splitext(video_url)[1][1:] - video_date = re.sub('-', '', clip['created_on'][:10]) + video_date = re.sub('-', '', clip['start_time'][:10]) + video_uploader_id = clip.get('user_id', clip.get('channel_id')) info.append({ 'id': clip['id'], 'url': video_url, 'title': clip['title'], - 'uploader': clip.get('user_id', clip.get('channel_id')), + 'uploader': clip.get('channel_name', video_uploader_id), + 'uploader_id': video_uploader_id, 'upload_date': video_date, 'ext': video_extension, }) @@ -3571,7 +3559,7 @@ class JustinTVIE(InfoExtractor): paged = True api += '/channel/archives/%s.json' else: - api += '/clip/show/%s.json' + api += '/broadcast/by_archive/%s.json' api = api % (video_id,) self.report_extraction(video_id) @@ -3711,11 +3699,11 @@ class SteamIE(InfoExtractor): } videos.append(info) return videos - + class UstreamIE(InfoExtractor): - _VALID_URL = r'http://www.ustream.tv/recorded/(?P<videoID>\d+)' + _VALID_URL = r'https?://www\.ustream\.tv/recorded/(?P<videoID>\d+)' IE_NAME = u'ustream' - + def _real_extract(self, url): m = re.match(self._VALID_URL, url) video_id = m.group('videoID') diff --git a/youtube_dl/PostProcessor.py b/youtube_dl/PostProcessor.py index a04828518..545b6992b 100644 --- a/youtube_dl/PostProcessor.py +++ b/youtube_dl/PostProcessor.py @@ -45,31 +45,24 @@ class PostProcessor(object): one has an extra field called "filepath" that points to the downloaded file. - When this method returns None, the postprocessing chain is - stopped. However, this method may return an information - dictionary that will be passed to the next postprocessing - object in the chain. It can be the one it received after - changing some fields. + This method returns a tuple, the first element of which describes + whether the original file should be kept (i.e. not deleted - None for + no preference), and the second of which is the updated information. In addition, this method may raise a PostProcessingError - exception that will be taken into account by the downloader - it was called from. + exception if post processing fails. """ - return information # by default, do nothing + return None, information # by default, keep file and do nothing -class AudioConversionError(BaseException): - def __init__(self, message): - self.message = message +class FFmpegPostProcessorError(PostProcessingError): + pass -class FFmpegExtractAudioPP(PostProcessor): - def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False, nopostoverwrites=False): +class AudioConversionError(PostProcessingError): + pass + +class FFmpegPostProcessor(PostProcessor): + def __init__(self,downloader=None): PostProcessor.__init__(self, downloader) - if preferredcodec is None: - preferredcodec = 'best' - self._preferredcodec = preferredcodec - self._preferredquality = preferredquality - self._keepvideo = keepvideo - self._nopostoverwrites = nopostoverwrites self._exes = self.detect_executables() @staticmethod @@ -83,10 +76,37 @@ class FFmpegExtractAudioPP(PostProcessor): programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] return dict((program, executable(program)) for program in programs) + def run_ffmpeg(self, path, out_path, opts): + if not self._exes['ffmpeg'] and not self._exes['avconv']: + raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.') + cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path)] + + opts + + [encodeFilename(self._ffmpeg_filename_argument(out_path))]) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout,stderr = p.communicate() + if p.returncode != 0: + msg = stderr.strip().split('\n')[-1] + raise FFmpegPostProcessorError(msg.decode('utf-8', 'replace')) + + def _ffmpeg_filename_argument(self, fn): + # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details + if fn.startswith(u'-'): + return u'./' + fn + return fn + +class FFmpegExtractAudioPP(FFmpegPostProcessor): + def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False): + FFmpegPostProcessor.__init__(self, downloader) + if preferredcodec is None: + preferredcodec = 'best' + self._preferredcodec = preferredcodec + self._preferredquality = preferredquality + self._nopostoverwrites = nopostoverwrites + def get_audio_codec(self, path): if not self._exes['ffprobe'] and not self._exes['avprobe']: return None try: - cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', '--', encodeFilename(path)] + cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))] handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE) output = handle.communicate()[0] if handle.wait() != 0: @@ -108,22 +128,18 @@ class FFmpegExtractAudioPP(PostProcessor): acodec_opts = [] else: acodec_opts = ['-acodec', codec] - cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path), '-vn'] - + acodec_opts + more_opts + - ['--', encodeFilename(out_path)]) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout,stderr = p.communicate() - if p.returncode != 0: - msg = stderr.strip().split('\n')[-1] - raise AudioConversionError(msg) + opts = ['-vn'] + acodec_opts + more_opts + try: + FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts) + except FFmpegPostProcessorError as err: + raise AudioConversionError(err.message) def run(self, information): path = information['filepath'] filecodec = self.get_audio_codec(path) if filecodec is None: - self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe') - return None + raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe') more_opts = [] if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'): @@ -132,7 +148,7 @@ class FFmpegExtractAudioPP(PostProcessor): acodec = 'copy' extension = self._preferredcodec more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc'] - elif filecodec in ['aac', 'mp3', 'vorbis']: + elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']: # Lossless if possible acodec = 'copy' extension = filecodec @@ -152,7 +168,7 @@ class FFmpegExtractAudioPP(PostProcessor): more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k'] else: # We convert the audio (lossy) - acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec] + acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec] extension = self._preferredcodec more_opts = [] if self._preferredquality is not None: @@ -181,10 +197,10 @@ class FFmpegExtractAudioPP(PostProcessor): except: etype,e,tb = sys.exc_info() if isinstance(e, AudioConversionError): - self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message) + msg = u'audio conversion failed: ' + e.message else: - self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')) - return None + msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + raise PostProcessingError(msg) # Try to update the date time for extracted audio file. if information.get('filetime') is not None: @@ -193,12 +209,24 @@ class FFmpegExtractAudioPP(PostProcessor): except: self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file') - if not self._keepvideo: - try: - os.remove(encodeFilename(path)) - except (IOError, OSError): - self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file') - return None - information['filepath'] = new_path - return information + return False,information + +class FFmpegVideoConvertor(FFmpegPostProcessor): + def __init__(self, downloader=None,preferedformat=None): + super(FFmpegVideoConvertor, self).__init__(downloader) + self._preferedformat=preferedformat + + def run(self, information): + path = information['filepath'] + prefix, sep, ext = path.rpartition(u'.') + outpath = prefix + sep + self._preferedformat + if information['ext'] == self._preferedformat: + self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat)) + return True,information + self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath) + self.run_ffmpeg(path, outpath, []) + information['filepath'] = outpath + information['format'] = self._preferedformat + information['ext'] = self._preferedformat + return False,information diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 1d914709f..ae12128b9 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -175,7 +175,6 @@ def parseOpts(): action='store', dest='subtitleslang', metavar='LANG', help='language of the closed captions to download (optional) use IETF language tags like \'en\'') - verbosity.add_option('-q', '--quiet', action='store_true', dest='quiet', help='activates quiet mode', default=False) verbosity.add_option('-s', '--simulate', @@ -248,9 +247,11 @@ def parseOpts(): postproc.add_option('-x', '--extract-audio', action='store_true', dest='extractaudio', default=False, help='convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)') postproc.add_option('--audio-format', metavar='FORMAT', dest='audioformat', default='best', - help='"best", "aac", "vorbis", "mp3", "m4a", or "wav"; best by default') + help='"best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; best by default') postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5', help='ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5)') + postproc.add_option('--recode-video', metavar='FORMAT', dest='recodevideo', default=None, + help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm)') postproc.add_option('-k', '--keep-video', action='store_true', dest='keepvideo', default=False, help='keeps the video file on disk after the post-processing; the video is erased by default') postproc.add_option('--no-post-overwrites', action='store_true', dest='nopostoverwrites', default=False, @@ -278,6 +279,10 @@ def parseOpts(): def _real_main(): parser, opts, args = parseOpts() + # Update version + if opts.update_self: + update_self(fd.to_screen, opts.verbose, sys.argv[0]) + # Open appropriate CookieJar if opts.cookiefile is None: jar = compat_cookiejar.CookieJar() @@ -370,12 +375,15 @@ def _real_main(): except (TypeError, ValueError) as err: parser.error(u'invalid playlist end number specified') if opts.extractaudio: - if opts.audioformat not in ['best', 'aac', 'mp3', 'vorbis', 'm4a', 'wav']: + if opts.audioformat not in ['best', 'aac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']: parser.error(u'invalid audio format specified') if opts.audioquality: opts.audioquality = opts.audioquality.strip('k').strip('K') if not opts.audioquality.isdigit(): parser.error(u'invalid audio quality specified') + if opts.recodevideo is not None: + if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg']: + parser.error(u'invalid video recode format specified') if sys.version_info < (3,): # In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems) @@ -432,6 +440,7 @@ def _real_main(): 'prefer_free_formats': opts.prefer_free_formats, 'verbose': opts.verbose, 'test': opts.test, + 'keepvideo': opts.keepvideo, }) if opts.verbose: @@ -453,11 +462,9 @@ def _real_main(): # PostProcessors if opts.extractaudio: - fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, keepvideo=opts.keepvideo, nopostoverwrites=opts.nopostoverwrites)) - - # Update version - if opts.update_self: - update_self(fd.to_screen, opts.verbose, sys.argv[0]) + fd.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites)) + if opts.recodevideo: + fd.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo)) # Maybe do nothing if len(all_urls) < 1: diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index 8f856ee8c..0e37390a2 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -450,7 +450,8 @@ class PostProcessingError(Exception): This exception may be raised by PostProcessor's .run() method to indicate an error in the postprocessing task. """ - pass + def __init__(self, msg): + self.msg = msg class MaxDownloadsReached(Exception): """ --max-downloads limit has been reached. """ diff --git a/youtube_dl/version.py b/youtube_dl/version.py index a4e9d2478..d8e82f4cd 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.01.02' +__version__ = '2013.01.11' |